//! LLM prompt validation integration test.
//!
//! Generates an LLM prompt for a PID via the generator CLI, feeds it to the `claude` CLI
//! to produce BO4E JSON, then reverse-maps the JSON back to EDIFACT segments.
//!
//! Two modes:
//! - **easy** (default): single attempt, pass if any segments are produced
//! - **strict**: retry with feedback up to N times, require >= 3 segments
//!
//! Run with: `cargo test -p mig-bo4e --test llm_prompt_validation_test -- --ignored`
//!
//! Environment variables:
//! - `TEST_PIDS` — comma-separated PID filter (default: all configured PIDs)
//! - `LLM_VALIDATION_MODE` — "easy" (default) or "strict"
//! - `LLM_MAX_RETRIES` — max retry attempts in strict mode (default: 3)

mod common;

use common::test_utils::MessageTypeConfig;
use edifact_primitives::EdifactDelimiters;
use mig_assembly::disassembler::Disassembler;
use mig_assembly::renderer::render_edifact;
use mig_bo4e::engine::MappingEngine;
use mig_bo4e::model::{MappedMessage, MappedTransaktion};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;

// ── Part A: CLI Helpers ──

/// Check if `claude` CLI is available on PATH.
fn claude_available() -> bool {
    Command::new("claude")
        .arg("--version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

/// Check if the generator binary can be built/found.
fn generator_available() -> bool {
    Command::new("cargo")
        .args(["build", "-p", "automapper-generator"])
        .current_dir("../../")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

/// Generate a prompt Markdown file by running the generator CLI.
///
/// Output goes to `target/llm-validation/prompts/{fv}/{msg}_{variant}/pid_{pid}.md`
/// relative to the workspace root.
fn generate_prompt(
    pid: &str,
    format_version: &str,
    message_type: &str,
    variant: Option<&str>,
) -> Option<PathBuf> {
    let output_dir = Path::new("target/llm-validation/prompts");
    let mut cmd = Command::new("cargo");
    cmd.args([
        "run",
        "-p",
        "automapper-generator",
        "--",
        "generate-llm-prompt",
        "--pid",
        pid,
        "--format-version",
        format_version,
        "--message-type",
        message_type,
        "--output-dir",
    ])
    .arg(output_dir)
    .current_dir("../../");

    if let Some(v) = variant {
        cmd.args(["--variant", v]);
    }

    let output = cmd.output().ok()?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("generate-llm-prompt failed for PID {pid}: {stderr}");
        return None;
    }

    // Build the expected output path
    let variant_suffix = match variant {
        Some(v) => format!("{}_{}", message_type, v),
        None => message_type.to_string(),
    };
    let prompt_path = output_dir
        .join(format_version)
        .join(&variant_suffix)
        .join(format!("pid_{pid}.md"));

    // The output_dir is relative to workspace root (where generator runs).
    // Resolve from test crate dir by prefixing ../../
    let from_test_crate = Path::new("../../").join(&prompt_path);
    if from_test_crate.exists() {
        Some(from_test_crate)
    } else {
        eprintln!("Prompt file not found at {from_test_crate:?}");
        None
    }
}

/// Call `claude --print --output-format text` with a prompt on stdin, return stdout.
fn call_claude(prompt: &str) -> Option<String> {
    use std::io::Write;
    let mut child = Command::new("claude")
        .args(["--print", "--output-format", "text"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .ok()?;

    if let Some(ref mut stdin) = child.stdin {
        stdin.write_all(prompt.as_bytes()).ok()?;
    }
    // Drop stdin to signal EOF
    drop(child.stdin.take());

    let output = child.wait_with_output().ok()?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("claude CLI failed: {stderr}");
        return None;
    }

    Some(String::from_utf8_lossy(&output.stdout).to_string())
}

/// Extract JSON from a Claude response.
///
/// Tries direct parse first, then strips ```json fences.
fn extract_json(response: &str) -> Option<serde_json::Value> {
    // Try direct parse
    if let Ok(v) = serde_json::from_str::<serde_json::Value>(response.trim()) {
        return Some(v);
    }

    // Try stripping ```json ... ``` fences
    let trimmed = response.trim();
    if let Some(start) = trimmed.find("```json") {
        let after_fence = &trimmed[start + 7..];
        if let Some(end) = after_fence.find("```") {
            let json_str = after_fence[..end].trim();
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(json_str) {
                return Some(v);
            }
        }
    }

    // Try stripping plain ``` fences
    if let Some(start) = trimmed.find("```") {
        let after_fence = &trimmed[start + 3..];
        // Skip optional language tag on the same line
        let after_lang = if let Some(nl) = after_fence.find('\n') {
            &after_fence[nl + 1..]
        } else {
            after_fence
        };
        if let Some(end) = after_lang.find("```") {
            let json_str = after_lang[..end].trim();
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(json_str) {
                return Some(v);
            }
        }
    }

    None
}

/// Save an artifact file to `target/llm-validation/{pid}/{filename}`.
fn save_artifact(pid: &str, filename: &str, content: &str) {
    let dir = Path::new("../../target/llm-validation").join(pid);
    let _ = std::fs::create_dir_all(&dir);
    let path = dir.join(filename);
    let _ = std::fs::write(&path, content);
    eprintln!("  Saved artifact: {}", path.display());
}

// ── Part B: PID Config ──

struct PidTestConfig {
    pid: &'static str,
    format_version: &'static str,
    message_type: &'static str,
    variant: Option<&'static str>,
    tx_group: &'static str,
    mappings_base: &'static str,
    schema_dir: &'static str,
}

impl PidTestConfig {
    fn as_message_type_config(&self) -> MessageTypeConfig {
        MessageTypeConfig {
            fixture_dir: "", // Not used for LLM validation
            mappings_base: self.mappings_base,
            schema_dir: self.schema_dir,
            message_type: self.message_type,
            variant: self.variant,
            tx_group: self.tx_group,
            format_version: self.format_version,
        }
    }
}

const PID_CONFIGS: &[PidTestConfig] = &[
    PidTestConfig {
        pid: "55001",
        format_version: "FV2504",
        message_type: "UTILMD",
        variant: Some("Strom"),
        tx_group: "SG4",
        mappings_base: "../../mappings/FV2504/UTILMD_Strom",
        schema_dir: "../../crates/mig-types/src/generated/fv2504/utilmd/pids",
    },
    PidTestConfig {
        pid: "17001",
        format_version: "FV2504",
        message_type: "ORDERS",
        variant: None,
        tx_group: "SG29",
        mappings_base: "../../mappings/FV2504/ORDERS",
        schema_dir: "../../crates/mig-types/src/generated/fv2504/orders/pids",
    },
    PidTestConfig {
        pid: "13002",
        format_version: "FV2504",
        message_type: "MSCONS",
        variant: None,
        tx_group: "SG5",
        mappings_base: "../../mappings/FV2504/MSCONS",
        schema_dir: "../../crates/mig-types/src/generated/fv2504/mscons/pids",
    },
];

fn get_test_configs() -> Vec<&'static PidTestConfig> {
    let filter = std::env::var("TEST_PIDS").ok();
    match filter {
        Some(pids) => {
            let wanted: Vec<&str> = pids.split(',').map(|s| s.trim()).collect();
            PID_CONFIGS
                .iter()
                .filter(|c| wanted.contains(&c.pid))
                .collect()
        }
        None => PID_CONFIGS.iter().collect(),
    }
}

fn get_mode() -> String {
    std::env::var("LLM_VALIDATION_MODE").unwrap_or_else(|_| "easy".to_string())
}

fn get_max_retries() -> usize {
    std::env::var("LLM_MAX_RETRIES")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(3)
}

// ── Part C: Easy Mode ──

/// Run easy mode validation for a PID.
///
/// Returns (success, json_value, diagnostic_message).
fn run_easy_mode(
    config: &PidTestConfig,
    prompt_md: &str,
) -> (bool, Option<serde_json::Value>, String) {
    let system_prompt = format!(
        "You are given documentation for PID {}. \
         Generate a single valid BO4E JSON object that represents a complete message \
         for this PID. Return ONLY valid JSON, no markdown fences, no explanation.",
        config.pid
    );
    let full_prompt = format!("{}\n\n{}", system_prompt, prompt_md);

    let response = match call_claude(&full_prompt) {
        Some(r) => r,
        None => return (false, None, "claude CLI call failed".to_string()),
    };

    save_artifact(config.pid, "claude_response.txt", &response);

    let json = match extract_json(&response) {
        Some(j) => j,
        None => {
            save_artifact(config.pid, "parse_error.txt", &response);
            return (
                false,
                None,
                "Failed to extract JSON from response".to_string(),
            );
        }
    };

    save_artifact(
        config.pid,
        "bo4e.json",
        &serde_json::to_string_pretty(&json).unwrap_or_default(),
    );

    // Build MappedMessage from the JSON
    let mapped = build_mapped_message(&json, config.tx_group);

    // Load engines and try reverse mapping
    let mtc = config.as_message_type_config();
    let Some(filtered_mig) = mtc.load_pid_filtered_mig(config.pid) else {
        return (
            false,
            Some(json),
            "Failed to load PID-filtered MIG".to_string(),
        );
    };

    let (msg_engine, tx_engine) = mtc.load_split_engines(config.pid);

    let reverse_tree = MappingEngine::map_interchange_reverse(
        &msg_engine,
        &tx_engine,
        &mapped,
        config.tx_group,
        Some(&filtered_mig),
    );

    let disassembler = Disassembler::new(&filtered_mig);
    let delimiters = EdifactDelimiters::default();
    let dis_segments = disassembler.disassemble(&reverse_tree);
    let rendered = render_edifact(&dis_segments, &delimiters);

    let seg_count = dis_segments.len();
    save_artifact(config.pid, "reverse.edi", &rendered);

    let success = seg_count > 0;
    let msg = format!("Produced {} EDIFACT segments", seg_count);
    (success, Some(json), msg)
}

/// Lowercase first character of a string (PascalCase → camelCase).
/// The reverse mapper uses camelCase entity keys internally.
fn to_camel_case(name: &str) -> String {
    let mut chars = name.chars();
    match chars.next() {
        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
        None => String::new(),
    }
}

/// Build a `MappedMessage` from LLM-generated BO4E JSON.
///
/// All transaction-level entities (including Prozessdaten/Nachricht) go into
/// `MappedTransaktion.stammdaten`. Message-level entities (Marktteilnehmer,
/// Ansprechpartner) go into the message-level stammdaten.
/// All entity keys are converted to camelCase to match the reverse mapper.
fn build_mapped_message(json: &serde_json::Value, _tx_group: &str) -> MappedMessage {
    let obj = json.as_object().cloned().unwrap_or_default();

    let mut tx_stammdaten_map = serde_json::Map::new();
    let mut msg_stammdaten_map = serde_json::Map::new();

    for (key, value) in &obj {
        let camel_key = to_camel_case(key);
        match key.as_str() {
            // Message-level entities (SG2) stay at the message level
            "Marktteilnehmer" | "Ansprechpartner" | "marktteilnehmer" | "ansprechpartner" => {
                msg_stammdaten_map.insert(camel_key, value.clone());
            }
            _ => {
                tx_stammdaten_map.insert(camel_key, value.clone());
            }
        }
    }

    let tx = MappedTransaktion {
        stammdaten: serde_json::Value::Object(tx_stammdaten_map),
        nesting_info: HashMap::new(),
    };

    MappedMessage {
        stammdaten: serde_json::Value::Object(msg_stammdaten_map),
        transaktionen: vec![tx],
        nesting_info: HashMap::new(),
    }
}

// ── Part D: Strict Mode ──

/// Run strict mode validation with retry logic.
///
/// Returns (success, attempts_used, final_segment_count).
fn run_strict_mode(
    config: &PidTestConfig,
    prompt_md: &str,
    max_retries: usize,
) -> (bool, usize, usize) {
    let min_segments = 3;
    let mut last_seg_count = 0;

    for attempt in 0..=max_retries {
        let prompt = if attempt == 0 {
            let system_prompt = format!(
                "You are given documentation for PID {}. \
                 Generate a single valid BO4E JSON object that represents a complete message \
                 for this PID. Return ONLY valid JSON, no markdown fences, no explanation.",
                config.pid
            );
            format!("{}\n\n{}", system_prompt, prompt_md)
        } else {
            format!(
                "Your previous attempt produced only {} EDIFACT segments (need at least {}). \
                 The JSON was likely missing required entities or fields. \
                 Please try again with a more complete BO4E JSON.\n\n\
                 Original prompt:\n\n{}",
                last_seg_count, min_segments, prompt_md
            )
        };

        let response = match call_claude(&prompt) {
            Some(r) => r,
            None => {
                eprintln!(
                    "  PID {}: attempt {} - claude CLI call failed",
                    config.pid,
                    attempt + 1
                );
                continue;
            }
        };

        save_artifact(
            config.pid,
            &format!("claude_response_attempt_{}.txt", attempt + 1),
            &response,
        );

        let json = match extract_json(&response) {
            Some(j) => j,
            None => {
                eprintln!(
                    "  PID {}: attempt {} - failed to extract JSON",
                    config.pid,
                    attempt + 1
                );
                continue;
            }
        };

        let mapped = build_mapped_message(&json, config.tx_group);
        let mtc = config.as_message_type_config();
        let Some(filtered_mig) = mtc.load_pid_filtered_mig(config.pid) else {
            eprintln!("  PID {}: failed to load PID-filtered MIG", config.pid);
            return (false, attempt + 1, 0);
        };

        let (msg_engine, tx_engine) = mtc.load_split_engines(config.pid);

        let reverse_tree = MappingEngine::map_interchange_reverse(
            &msg_engine,
            &tx_engine,
            &mapped,
            config.tx_group,
            Some(&filtered_mig),
        );

        let disassembler = Disassembler::new(&filtered_mig);
        let delimiters = EdifactDelimiters::default();
        let dis_segments = disassembler.disassemble(&reverse_tree);
        let rendered = render_edifact(&dis_segments, &delimiters);
        last_seg_count = dis_segments.len();

        save_artifact(
            config.pid,
            &format!("reverse_attempt_{}.edi", attempt + 1),
            &rendered,
        );

        eprintln!(
            "  PID {}: attempt {} - {} segments",
            config.pid,
            attempt + 1,
            last_seg_count
        );

        if last_seg_count >= min_segments {
            return (true, attempt + 1, last_seg_count);
        }
    }

    (false, max_retries + 1, last_seg_count)
}

// ── Part E: Test Entry Point ──

#[test]
#[ignore] // Requires claude CLI — run with: cargo test -- --ignored
fn test_llm_prompt_validation() {
    if !claude_available() {
        eprintln!("SKIP: claude CLI not found");
        return;
    }
    if !generator_available() {
        eprintln!("SKIP: generator not built");
        return;
    }

    let configs = get_test_configs();
    if configs.is_empty() {
        eprintln!("No PID configs matched TEST_PIDS filter");
        return;
    }

    let mode = get_mode();
    let max_retries = get_max_retries();

    eprintln!(
        "LLM Prompt Validation: mode={}, max_retries={}, PIDs={}",
        mode,
        max_retries,
        configs.iter().map(|c| c.pid).collect::<Vec<_>>().join(", ")
    );

    let mut results: Vec<(&str, bool, String)> = Vec::new();

    for config in &configs {
        eprintln!(
            "\n--- PID {} ({} {}) ---",
            config.pid,
            config.message_type,
            config.variant.unwrap_or("")
        );

        // Step 1: Generate prompt
        let prompt_path = match generate_prompt(
            config.pid,
            config.format_version,
            config.message_type,
            config.variant,
        ) {
            Some(p) => p,
            None => {
                let msg = "Failed to generate prompt".to_string();
                eprintln!("  {}", msg);
                results.push((config.pid, false, msg));
                continue;
            }
        };

        eprintln!("  Prompt generated: {}", prompt_path.display());

        let prompt_md = match std::fs::read_to_string(&prompt_path) {
            Ok(s) => s,
            Err(e) => {
                let msg = format!("Failed to read prompt: {e}");
                eprintln!("  {}", msg);
                results.push((config.pid, false, msg));
                continue;
            }
        };

        // Step 2: Run easy or strict mode
        match mode.as_str() {
            "strict" => {
                let (success, attempts, seg_count) =
                    run_strict_mode(config, &prompt_md, max_retries);
                let msg = format!(
                    "strict: {} segments after {} attempt(s)",
                    seg_count, attempts
                );
                eprintln!(
                    "  Result: {} - {}",
                    if success { "PASS" } else { "FAIL" },
                    msg
                );
                results.push((config.pid, success, msg));
            }
            _ => {
                let (success, _json, msg) = run_easy_mode(config, &prompt_md);
                eprintln!(
                    "  Result: {} - {}",
                    if success { "PASS" } else { "FAIL" },
                    msg
                );
                results.push((config.pid, success, msg));
            }
        }
    }

    // Summary
    eprintln!("\n=== LLM Prompt Validation Summary ===");
    let mut any_failed = false;
    for (pid, success, msg) in &results {
        let status = if *success { "PASS" } else { "FAIL" };
        eprintln!("  PID {pid}: {status} - {msg}");
        if !*success {
            any_failed = true;
        }
    }

    if any_failed {
        panic!(
            "LLM prompt validation failed for some PIDs. \
             Check target/llm-validation/ for artifacts."
        );
    }
}

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

    #[test]
    fn test_extract_json_direct() {
        let input = r#"{"Marktlokation": {"id": "12345"}}"#;
        let result = extract_json(input);
        assert!(result.is_some());
        assert!(result.unwrap()["Marktlokation"]["id"] == "12345");
    }

    #[test]
    fn test_extract_json_with_fences() {
        let input = "Here is the JSON:\n```json\n{\"key\": \"value\"}\n```\nDone.";
        let result = extract_json(input);
        assert!(result.is_some());
        assert!(result.unwrap()["key"] == "value");
    }

    #[test]
    fn test_extract_json_invalid() {
        let input = "This is not JSON at all";
        let result = extract_json(input);
        assert!(result.is_none());
    }

    #[test]
    fn test_build_mapped_message_structure() {
        let json = serde_json::json!({
            "Prozessdaten": {"referenz": "ABC"},
            "Nachricht": {"typ": "UTILMD"},
            "Marktlokation": {"id": "DE123"},
            "Marktteilnehmer": [{"rolle": "LF"}]
        });

        let mapped = build_mapped_message(&json, "SG4");

        // All transaction-level entities go to stammdaten (camelCase keys)
        assert!(mapped.transaktionen.len() == 1);
        let tx = &mapped.transaktionen[0];
        assert!(tx.stammdaten["prozessdaten"].is_object());
        assert!(tx.stammdaten["nachricht"].is_object());

        // Marktlokation goes to tx stammdaten (camelCase)
        assert!(tx.stammdaten["marktlokation"].is_object());

        // Marktteilnehmer goes to message-level stammdaten (camelCase)
        assert!(mapped.stammdaten["marktteilnehmer"].is_array());
    }
}
