use std::env;
use std::error::Error;
use std::io;

use mdforge::{ArgType, EvalContext, Forge};
use serde_json::{json, Value};

trait AiMarkdownClient {
    fn generate_markdown(&self, prompt: &str) -> Result<String, Box<dyn Error>>;
}

struct MockAiMarkdownClient;

impl AiMarkdownClient for MockAiMarkdownClient {
    fn generate_markdown(&self, prompt: &str) -> Result<String, Box<dyn Error>> {
        assert!(prompt.contains("Block: card"));
        assert!(prompt.contains("Inline: badge"));
        Ok(":::card title=hello kind=info\nBody {badge level=2}\n:::\n".to_string())
    }
}

struct OpenAiMarkdownClient {
    api_key: String,
    model: String,
}

impl OpenAiMarkdownClient {
    fn from_env() -> Result<Self, Box<dyn Error>> {
        let api_key = env::var("OPENAI_API_KEY")
            .map_err(|_| "OPENAI_API_KEY is required when MDFORGE_RUN_OPENAI_TESTS=1")?;
        let model = env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4.1-mini".to_string());
        Ok(Self { api_key, model })
    }
}

impl AiMarkdownClient for OpenAiMarkdownClient {
    fn generate_markdown(&self, prompt: &str) -> Result<String, Box<dyn Error>> {
        let client = reqwest::blocking::Client::new();
        let response = client
            .post("https://api.openai.com/v1/responses")
            .bearer_auth(&self.api_key)
            .json(&json!({
                "model": self.model,
                "input": prompt,
                "max_output_tokens": 120,
                "store": false
            }))
            .send()?;

        let status = response.status();
        let body: Value = response.json()?;
        if !status.is_success() {
            return Err(format!("OpenAI request failed with {status}: {body}").into());
        }

        extract_response_text(&body)
            .ok_or_else(|| format!("OpenAI response did not contain text output: {body}").into())
    }
}

fn extract_response_text(response: &Value) -> Option<String> {
    response
        .get("output")
        .and_then(Value::as_array)?
        .iter()
        .flat_map(|item| {
            item.get("content")
                .and_then(Value::as_array)
                .into_iter()
                .flatten()
        })
        .find_map(|content| {
            content
                .get("text")
                .and_then(Value::as_str)
                .map(str::to_string)
        })
}

fn test_forge() -> Forge {
    Forge::builder()
        .block("card")
        .arg("title", ArgType::String.required())
        .arg("kind", ArgType::StaticEnum(&["info", "warn"]).required())
        .body_markdown()
        .register()
        .inline("badge")
        .arg("level", ArgType::Int.required())
        .register()
        .build()
}

fn prompt_for(forge: &Forge) -> String {
    format!(
        "Return only raw mdforge text. Do not use Markdown code fences. Do not add explanation.\n\
         Use only this syntax:\n{}\n\
         Your entire response must be exactly these 3 lines:\n\
         :::card title=hello kind=info\n\
         Body {{badge level=2}}\n\
         :::",
        forge.signature()
    )
}

fn assert_ai_output_is_valid_mdforge_document(
    client: &dyn AiMarkdownClient,
) -> Result<(), Box<dyn Error>> {
    let forge = test_forge();
    let markdown = client.generate_markdown(&prompt_for(&forge))?;
    let doc = forge
        .parse(markdown.trim_end())
        .map_err(|diagnostics| diagnostic_error("parse", &markdown, diagnostics))?;

    forge
        .validate(&doc)
        .map_err(|diagnostics| diagnostic_error("validate", &markdown, diagnostics))?;
    forge
        .eval(&doc, &EvalContext::default())
        .map_err(|diagnostics| diagnostic_error("eval", &markdown, diagnostics))?;

    Ok(())
}

fn diagnostic_error(
    stage: &str,
    markdown: &str,
    diagnostics: Vec<mdforge::Diagnostic>,
) -> io::Error {
    io::Error::new(
        io::ErrorKind::Other,
        format!("{stage} failed for AI markdown:\n{markdown}\nDiagnostics: {diagnostics:?}"),
    )
}

#[test]
fn ai_pipeline_uses_mock_by_default() -> Result<(), Box<dyn Error>> {
    assert_ai_output_is_valid_mdforge_document(&MockAiMarkdownClient)
}

#[test]
fn ai_pipeline_can_call_openai_when_enabled() -> Result<(), Box<dyn Error>> {
    if env::var("MDFORGE_RUN_OPENAI_TESTS").as_deref() != Ok("1") {
        eprintln!("skipping live OpenAI test; set MDFORGE_RUN_OPENAI_TESTS=1 to enable it");
        return Ok(());
    }

    let client = OpenAiMarkdownClient::from_env()?;
    assert_ai_output_is_valid_mdforge_document(&client)
}
