use anyhow::{Context, Result};
use serde::Serialize;
use serde_json::{json, Map, Value};

use crate::symbolic_prompt;

const MAX_ITEMS: usize = 3;
const MAX_MEMORY_ITEMS: usize = 2;
const MAX_PAYLOAD_BYTES: usize = 140;

#[derive(Clone, Copy)]
pub(crate) enum JsonSurface {
    Start,
    ContextCheck,
    Handover,
    Checkpoint,
}

#[derive(Serialize)]
pub(crate) struct PromptInjectionView {
    pub(crate) enabled: bool,
    pub(crate) format: &'static str,
    pub(crate) dialect: &'static str,
    pub(crate) status: &'static str,
    pub(crate) fallback_projection: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) warning: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) frame: Option<String>,
    pub(crate) metrics: PromptInjectionMetricsView,
}

#[derive(Serialize)]
pub(crate) struct PromptInjectionMetricsView {
    pub(crate) source_fingerprint: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) frame_bytes: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) token_estimate: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) fallback_reason: Option<&'static str>,
    pub(crate) fingerprint_reuse: bool,
    pub(crate) cache_reuse: bool,
    pub(crate) delta_applied: bool,
}

struct SurfaceRequest {
    source_fingerprint: String,
    action: String,
    fallback_projection: &'static str,
    input: Value,
}

pub(crate) fn insert_json_surface(
    value: &mut Value,
    surface: JsonSurface,
) -> Result<Option<String>> {
    if value.get("prompt_injection").is_some() {
        return Ok(None);
    }

    let Some(request) = surface_request(value, surface) else {
        let (view, warning) = skipped_view(surface_fallback_projection(surface));
        let object = value
            .as_object_mut()
            .context("report is not a JSON object")?;
        object.insert("prompt_injection".to_owned(), serde_json::to_value(view)?);
        return Ok(Some(warning));
    };
    let fail_closed = action_requires_fail_closed(&request.action);
    let (view, warning) = render_from_input(
        &request.input,
        &request.source_fingerprint,
        &request.action,
        fail_closed,
        request.fallback_projection,
        false,
        false,
        false,
    );
    let object = value
        .as_object_mut()
        .context("report is not a JSON object")?;
    object.insert("prompt_injection".to_owned(), serde_json::to_value(view)?);
    Ok(warning)
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn render_from_input(
    input: &Value,
    source_fingerprint: &str,
    action: &str,
    fail_closed: bool,
    fallback_projection: &'static str,
    fingerprint_reuse: bool,
    cache_reuse: bool,
    delta_applied: bool,
) -> (PromptInjectionView, Option<String>) {
    let token_estimate = estimate_tokens(input);
    let rendered = symbolic_prompt::RenderRequest::from_runtime_value(input, token_estimate)
        .and_then(|request| symbolic_prompt::render_frame(&request));
    prompt_injection_view(
        source_fingerprint,
        Some(token_estimate),
        fingerprint_reuse,
        cache_reuse,
        delta_applied,
        fallback_projection,
        action,
        fail_closed,
        rendered,
    )
}

pub(crate) fn action_requires_fail_closed(action: &str) -> bool {
    matches!(action, "stop" | "wrap_up" | "renew" | "escalate")
}

#[allow(clippy::too_many_arguments)]
fn prompt_injection_view(
    source_fingerprint: &str,
    token_estimate: Option<u64>,
    fingerprint_reuse: bool,
    cache_reuse: bool,
    delta_applied: bool,
    fallback_projection: &'static str,
    action: &str,
    fail_closed: bool,
    rendered: Result<symbolic_prompt::RenderedFrame, symbolic_prompt::FallbackReason>,
) -> (PromptInjectionView, Option<String>) {
    match rendered {
        Ok(rendered) => (
            PromptInjectionView {
                enabled: true,
                format: "ccd1",
                dialect: symbolic_prompt::DIALECT,
                status: "rendered",
                fallback_projection,
                warning: None,
                frame: Some(rendered.frame),
                metrics: PromptInjectionMetricsView {
                    source_fingerprint: source_fingerprint.to_owned(),
                    frame_bytes: Some(rendered.metrics.bytes),
                    token_estimate: Some(rendered.metrics.token_estimate),
                    fallback_reason: None,
                    fingerprint_reuse,
                    cache_reuse,
                    delta_applied,
                },
            },
            None,
        ),
        Err(reason) => {
            let status = if fail_closed { "blocked" } else { "fallback" };
            let warning = format!(
                "CCD1 prompt injection {status} for action `{action}`: {}; {fallback_projection} retained",
                reason.as_str()
            );
            (
                PromptInjectionView {
                    enabled: true,
                    format: "ccd1",
                    dialect: symbolic_prompt::DIALECT,
                    status,
                    fallback_projection,
                    warning: Some(warning.clone()),
                    frame: None,
                    metrics: PromptInjectionMetricsView {
                        source_fingerprint: source_fingerprint.to_owned(),
                        frame_bytes: None,
                        token_estimate,
                        fallback_reason: Some(reason.as_str()),
                        fingerprint_reuse,
                        cache_reuse,
                        delta_applied,
                    },
                },
                Some(warning),
            )
        }
    }
}

fn skipped_view(fallback_projection: &'static str) -> (PromptInjectionView, String) {
    let reason = "missing_source_data";
    let warning =
        format!("CCD1 prompt injection skipped: {reason}; {fallback_projection} retained");
    (
        PromptInjectionView {
            enabled: true,
            format: "ccd1",
            dialect: symbolic_prompt::DIALECT,
            status: "skipped",
            fallback_projection,
            warning: Some(warning.clone()),
            frame: None,
            metrics: PromptInjectionMetricsView {
                source_fingerprint: "unknown".to_owned(),
                frame_bytes: None,
                token_estimate: None,
                fallback_reason: Some(reason),
                fingerprint_reuse: false,
                cache_reuse: false,
                delta_applied: false,
            },
        },
        warning,
    )
}

fn surface_request(value: &Value, surface: JsonSurface) -> Option<SurfaceRequest> {
    match surface {
        JsonSurface::Start => start_request(value),
        JsonSurface::ContextCheck => context_check_request(value),
        JsonSurface::Handover => radar_request(value, surface_fallback_projection(surface)),
        JsonSurface::Checkpoint => radar_request(value, surface_fallback_projection(surface)),
    }
}

fn surface_fallback_projection(surface: JsonSurface) -> &'static str {
    match surface {
        JsonSurface::Start => "json_start",
        JsonSurface::ContextCheck => "json_context_check",
        JsonSurface::Handover => "json_handover",
        JsonSurface::Checkpoint => "json_checkpoint",
    }
}

fn start_request(value: &Value) -> Option<SurfaceRequest> {
    let source_fingerprint = pointer_string(value, "/compiled_state/source_fingerprint")?;
    let action = pointer_string(value, "/session_boundary/action")?;
    let mut input = base_input(&source_fingerprint, &action, symbolic_prompt::Mode::Startup);
    insert_string(
        &mut input,
        "title",
        pointer_string(value, "/compact_summary/current_state/title"),
    );
    let mut next = strings_at(
        value,
        "/compact_summary/next_focus/continuity_immediate_actions",
        MAX_ITEMS,
    );
    if let Some(anchor) = pointer_string(value, "/compact_summary/next_focus/execution_gate_anchor")
    {
        next.push(anchor);
    }
    insert_strings(&mut input, "next", next);
    insert_strings(
        &mut input,
        "rules",
        strings_at(
            value,
            "/compact_summary/active_guardrails/operational_guardrails",
            2,
        ),
    );
    insert_strings(&mut input, "memory", start_memory(value));
    insert_strings(&mut input, "blockers", boundary_blockers(value));

    Some(SurfaceRequest {
        source_fingerprint,
        action,
        fallback_projection: surface_fallback_projection(JsonSurface::Start),
        input: Value::Object(input),
    })
}

fn context_check_request(value: &Value) -> Option<SurfaceRequest> {
    let source_fingerprint = pointer_string(value, "/source_fingerprint")?;
    let action = context_check_symbolic_action(value)
        .or_else(|| pointer_string(value, "/session_boundary/action"))?;
    let mut input = base_input(&source_fingerprint, &action, symbolic_prompt::Mode::Steady);
    insert_string(
        &mut input,
        "title",
        pointer_string(value, "/payload/focus_digest"),
    );
    insert_strings(
        &mut input,
        "next",
        strings_from_values([
            pointer_string(value, "/recommendation"),
            pointer_string(value, "/payload/state_delta"),
        ]),
    );
    insert_strings(
        &mut input,
        "rules",
        strings_from_values([pointer_string(value, "/payload/policy_digest")]),
    );
    let mut blockers = Vec::new();
    if action_requires_fail_closed(&action)
        || matches!(
            context_check_action(value).as_deref(),
            Some("flush_for_compaction")
        )
    {
        blockers.extend(strings_from_values([pointer_string(
            value,
            "/payload/risk_summary",
        )]));
        blockers.extend(strings_at(value, "/evidence", MAX_ITEMS));
    }
    insert_strings(&mut input, "blockers", blockers);

    Some(SurfaceRequest {
        source_fingerprint,
        action,
        fallback_projection: surface_fallback_projection(JsonSurface::ContextCheck),
        input: Value::Object(input),
    })
}

fn radar_request(value: &Value, fallback_projection: &'static str) -> Option<SurfaceRequest> {
    let source_fingerprint = pointer_string(value, "/projection_telemetry/source_fingerprint")?;
    let action = pointer_string(value, "/session_boundary/action")?;
    let mut input = base_input(&source_fingerprint, &action, symbolic_prompt::Mode::Steady);
    insert_string(&mut input, "title", pointer_string(value, "/handoff/title"));
    let mut next = strings_at(value, "/handoff/immediate_actions", MAX_ITEMS);
    if let Some(summary) = pointer_string(value, "/evaluation/next_step/summary") {
        next.push(summary);
    }
    insert_strings(&mut input, "next", next);
    insert_strings(
        &mut input,
        "rules",
        strings_at(value, "/handoff/definition_of_done", 2),
    );
    insert_strings(&mut input, "blockers", boundary_blockers(value));

    Some(SurfaceRequest {
        source_fingerprint,
        action,
        fallback_projection,
        input: Value::Object(input),
    })
}

fn base_input(
    source_fingerprint: &str,
    action: &str,
    mode: symbolic_prompt::Mode,
) -> Map<String, Value> {
    let mut input = Map::new();
    input.insert(
        "source_fingerprint".to_owned(),
        Value::String(source_fingerprint.to_owned()),
    );
    input.insert("mode".to_owned(), Value::String(mode.as_str().to_owned()));
    input.insert("action".to_owned(), Value::String(action.to_owned()));
    input
}

fn context_check_symbolic_action(value: &Value) -> Option<String> {
    match context_check_action(value)?.as_str() {
        "none" => Some("continue".to_owned()),
        "inject_delta" | "checkpoint_now" => Some("refresh".to_owned()),
        "flush_for_compaction" | "wrap_up_required" => Some("wrap_up".to_owned()),
        "renew_required" => Some("renew".to_owned()),
        "escalate" => Some("escalate".to_owned()),
        _ => None,
    }
}

fn context_check_action(value: &Value) -> Option<String> {
    pointer_string(value, "/action")
}

fn boundary_blockers(value: &Value) -> Vec<String> {
    let action = pointer_string(value, "/session_boundary/action");
    if !matches!(
        action.as_deref(),
        Some("stop" | "wrap_up" | "renew" | "escalate")
    ) {
        return Vec::new();
    }
    let mut blockers = strings_from_values([pointer_string(value, "/session_boundary/summary")]);
    blockers.extend(strings_at(value, "/session_boundary/evidence", MAX_ITEMS));
    blockers
}

fn start_memory(value: &Value) -> Vec<String> {
    let mut items = Vec::new();
    for pointer in [
        "/compact_summary/active_guardrails/effective_memory/profile",
        "/compact_summary/active_guardrails/effective_memory/locality",
        "/compact_summary/active_guardrails/effective_memory/branch",
        "/compact_summary/active_guardrails/effective_memory/clone",
    ] {
        items.extend(strings_at(value, pointer, MAX_MEMORY_ITEMS));
        if items.len() >= MAX_MEMORY_ITEMS {
            items.truncate(MAX_MEMORY_ITEMS);
            break;
        }
    }
    items
}

fn insert_string(input: &mut Map<String, Value>, key: &str, value: Option<String>) {
    if let Some(value) = value.and_then(|value| compact_text(&value)) {
        input.insert(key.to_owned(), Value::String(value));
    }
}

fn insert_strings(input: &mut Map<String, Value>, key: &str, values: Vec<String>) {
    let values = values
        .into_iter()
        .filter_map(|value| compact_text(&value))
        .collect::<Vec<_>>();
    if !values.is_empty() {
        input.insert(key.to_owned(), json!(values));
    }
}

fn strings_at(value: &Value, pointer: &str, limit: usize) -> Vec<String> {
    value
        .pointer(pointer)
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(Value::as_str)
        .filter_map(compact_text)
        .take(limit)
        .collect()
}

fn strings_from_values<const N: usize>(values: [Option<String>; N]) -> Vec<String> {
    values
        .into_iter()
        .flatten()
        .filter_map(|value| compact_text(&value))
        .collect()
}

fn pointer_string(value: &Value, pointer: &str) -> Option<String> {
    value
        .pointer(pointer)
        .and_then(Value::as_str)
        .and_then(compact_text)
}

fn compact_text(text: &str) -> Option<String> {
    let mut normalized = String::new();
    let mut in_whitespace = false;
    for ch in text.trim().chars() {
        if ch.is_whitespace() {
            in_whitespace = true;
            continue;
        }
        if in_whitespace && !normalized.is_empty() {
            normalized.push(' ');
        }
        normalized.push(ch);
        in_whitespace = false;
    }
    if normalized.is_empty() {
        return None;
    }
    if normalized.len() <= MAX_PAYLOAD_BYTES {
        return Some(normalized);
    }

    let mut truncated = String::new();
    for ch in normalized.chars() {
        if truncated.len() + ch.len_utf8() + 3 > MAX_PAYLOAD_BYTES {
            break;
        }
        truncated.push(ch);
    }
    truncated.push_str("...");
    Some(truncated)
}

fn estimate_tokens(input: &Value) -> u64 {
    let chars = serde_json::to_string(input)
        .map(|json| json.chars().count())
        .unwrap_or(4);
    let estimate = chars.div_ceil(4) as u64;
    estimate.clamp(1, symbolic_prompt::TOKEN_ESTIMATE_MAX)
}

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

    #[test]
    fn missing_inputs_emit_structured_skipped_prompt_injection() {
        let mut value = json!({
            "command": "start",
            "session_boundary": {"action": "continue"}
        });

        let warning = insert_json_surface(&mut value, JsonSurface::Start)
            .unwrap()
            .unwrap();

        assert!(warning.contains("missing_source_data"));
        let injection = &value["prompt_injection"];
        assert_eq!(injection["enabled"], true);
        assert_eq!(injection["status"], "skipped");
        assert_eq!(injection["fallback_projection"], "json_start");
        assert_eq!(
            injection["metrics"]["fallback_reason"],
            "missing_source_data"
        );
        assert_eq!(injection["metrics"]["source_fingerprint"], "unknown");
        assert!(injection.get("frame").is_none());
    }

    #[test]
    fn action_requires_fail_closed_classifies_boundary_actions() {
        for action in ["stop", "wrap_up", "renew", "escalate"] {
            assert!(
                action_requires_fail_closed(action),
                "{action} should fail closed"
            );
        }
        for action in ["continue", "refresh", "checkpoint_now", ""] {
            assert!(
                !action_requires_fail_closed(action),
                "{action} should not fail closed"
            );
        }
    }

    #[test]
    fn context_check_symbolic_actions_map_to_prompt_actions() {
        let cases = [
            ("none", Some("continue")),
            ("inject_delta", Some("refresh")),
            ("checkpoint_now", Some("refresh")),
            ("flush_for_compaction", Some("wrap_up")),
            ("renew_required", Some("renew")),
            ("wrap_up_required", Some("wrap_up")),
            ("escalate", Some("escalate")),
            ("unknown", None),
        ];

        for (raw_action, expected_action) in cases {
            let value = json!({ "action": raw_action });
            assert_eq!(
                context_check_symbolic_action(&value).as_deref(),
                expected_action
            );
        }
    }

    #[test]
    fn context_check_request_uses_symbolic_action_and_fail_closed_blockers() {
        let value = json!({
            "source_fingerprint": "fp",
            "action": "wrap_up_required",
            "recommendation": "capture before compaction",
            "payload": {
                "focus_digest": "Focused task",
                "state_delta": "State changed",
                "policy_digest": "Policy reminder",
                "risk_summary": "Context pressure is high"
            },
            "evidence": ["Evidence one", "Evidence two", "Evidence three"],
            "session_boundary": {"action": "continue"}
        });

        let request = context_check_request(&value).unwrap();

        assert_eq!(request.source_fingerprint, "fp");
        assert_eq!(request.action, "wrap_up");
        assert_eq!(request.fallback_projection, "json_context_check");
        assert_eq!(request.input["title"], "Focused task");
        assert_eq!(
            request.input["next"],
            json!(["capture before compaction", "State changed"])
        );
        assert_eq!(request.input["rules"], json!(["Policy reminder"]));
        assert_eq!(
            request.input["blockers"],
            json!([
                "Context pressure is high",
                "Evidence one",
                "Evidence two",
                "Evidence three"
            ])
        );
    }

    #[test]
    fn context_check_request_treats_flush_for_compaction_as_blocking() {
        let value = json!({
            "source_fingerprint": "fp",
            "action": "flush_for_compaction",
            "payload": {
                "risk_summary": "Must preserve the handoff"
            },
            "evidence": ["Evidence one"],
            "session_boundary": {"action": "continue"}
        });

        let request = context_check_request(&value).unwrap();

        assert_eq!(request.action, "wrap_up");
        assert_eq!(
            request.input["blockers"],
            json!(["Must preserve the handoff", "Evidence one"])
        );
    }

    #[test]
    fn boundary_blockers_only_for_closure_actions() {
        let open_value = json!({
            "session_boundary": {
                "action": "continue",
                "summary": "Keep working",
                "evidence": ["Open evidence"]
            }
        });
        assert!(boundary_blockers(&open_value).is_empty());

        let close_value = json!({
            "session_boundary": {
                "action": "wrap_up",
                "summary": "Need clean handoff",
                "evidence": ["First reason", "Second reason", "Third reason", "Fourth reason"]
            }
        });

        assert_eq!(
            boundary_blockers(&close_value),
            vec![
                "Need clean handoff".to_owned(),
                "First reason".to_owned(),
                "Second reason".to_owned(),
                "Third reason".to_owned()
            ]
        );
    }

    #[test]
    fn start_memory_limits_and_prioritizes_memory_sources() {
        let profile_first = json!({
            "compact_summary": {
                "active_guardrails": {
                    "effective_memory": {
                        "profile": ["Profile one", "Profile two", "Profile three"],
                        "locality": ["Locality one"],
                        "branch": ["Branch one"],
                        "clone": ["Clone one"]
                    }
                }
            }
        });
        assert_eq!(
            start_memory(&profile_first),
            vec!["Profile one".to_owned(), "Profile two".to_owned()]
        );

        let fallback_sources = json!({
            "compact_summary": {
                "active_guardrails": {
                    "effective_memory": {
                        "profile": [],
                        "locality": ["Locality one"],
                        "branch": ["Branch one"],
                        "clone": ["Clone one"]
                    }
                }
            }
        });
        assert_eq!(
            start_memory(&fallback_sources),
            vec!["Locality one".to_owned(), "Branch one".to_owned()]
        );
    }
}
