use anyhow::{anyhow, bail, Context, Result};
use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::fs;
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use crate::runtime;

const ACP_PROTOCOL_VERSION: u32 = 1;
const REDACTED: &str = runtime::redaction::REDACTED_MARKER;
static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct AcpSession {
    session_id: String,
    cwd: PathBuf,
    mcp_servers: Vec<Value>,
    orchestration_slug: Option<String>,
    trace_ids: Vec<String>,
    transcript: Vec<AcpTranscriptEntry>,
    cancelled: bool,
    created_at: String,
    updated_at: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct AcpTranscriptEntry {
    role: String,
    message_id: String,
    text: String,
    at: String,
}

pub fn serve_stdio(root: &Path) -> Result<()> {
    let stdin = io::stdin();
    let mut stdout = io::stdout();
    serve_lines(root, stdin.lock(), &mut stdout)
}

pub fn serve_lines<R: BufRead, W: Write>(root: &Path, reader: R, writer: &mut W) -> Result<()> {
    for line in reader.lines() {
        let line = line?;
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let request: Value = match serde_json::from_str(trimmed) {
            Ok(value) => value,
            Err(error) => {
                writeln!(
                    writer,
                    "{}",
                    jsonrpc_error(Value::Null, -32700, &error.to_string())
                )?;
                continue;
            }
        };
        for response in handle_request(root, request) {
            writeln!(writer, "{}", serde_json::to_string(&response)?)?;
        }
        writer.flush()?;
    }
    Ok(())
}

pub fn doctor(root: &Path) -> Value {
    let sessions_dir = sessions_dir(root);
    let agent_manifest = runtime::agents::agent_manifest("sdd-orchestrator");
    json!({
        "status": if agent_manifest.is_ok() { "pass" } else { "fail" },
        "protocolVersion": ACP_PROTOCOL_VERSION,
        "sdk": acp_sdk_marker(),
        "sessionsDir": runtime::platform::display_path(&sessions_dir),
        "sessionsDirExists": sessions_dir.exists(),
        "agent": "sdd-orchestrator",
        "capabilities": initialize_agent_capabilities(),
        "error": agent_manifest.err().map(|err| err.to_string()),
    })
}

pub fn config_actions(
    root: &Path,
    targets: &str,
    force: bool,
    dry_run: bool,
) -> Result<Vec<String>> {
    let mut actions = Vec::new();
    let selected = parse_targets(targets)?;
    if selected
        .iter()
        .any(|target| target == "zed" || target == "all")
    {
        let rel = ".zed/settings.json";
        let target = root.join(rel);
        let content = render_zed_acp_settings(root);
        if target.exists() && !force {
            actions.push(format!("skip existing {}", target.display()));
        } else if dry_run {
            actions.push(format!("dry-run write {}", target.display()));
        } else {
            if let Some(parent) = target.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::write(&target, content)?;
            actions.push(format!("write {}", target.display()));
        }
    }
    Ok(actions)
}

fn handle_request(root: &Path, request: Value) -> Vec<Value> {
    let id = request.get("id").cloned().unwrap_or(Value::Null);
    let method = request.get("method").and_then(Value::as_str).unwrap_or("");
    let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
    if id.is_null() && method == "session/cancel" {
        if let Err(error) = cancel_session(root, &params) {
            return vec![jsonrpc_error(Value::Null, -32603, &error.to_string())];
        }
        return Vec::new();
    }
    let result = match method {
        "initialize" => Ok(vec![jsonrpc_result(id.clone(), initialize_result(&params))]),
        "authenticate" => Ok(vec![jsonrpc_result(id.clone(), json!({}))]),
        "session/new" => create_session(root, id.clone(), &params),
        "session/load" => load_session(root, id.clone(), &params),
        "session/prompt" => prompt_session(root, id.clone(), &params),
        "session/cancel" => {
            cancel_session(root, &params).map(|_| vec![jsonrpc_result(id.clone(), json!({}))])
        }
        _ => Err(anyhow!("method not found: {method}")),
    };
    match result {
        Ok(responses) => responses,
        Err(error) => vec![jsonrpc_error(id, -32603, &error.to_string())],
    }
}

fn initialize_result(params: &Value) -> Value {
    let requested = params
        .get("protocolVersion")
        .and_then(Value::as_u64)
        .unwrap_or(ACP_PROTOCOL_VERSION as u64);
    json!({
        "protocolVersion": requested.min(ACP_PROTOCOL_VERSION as u64),
        "agentInfo": {
            "name": "sdd-layer",
            "version": env!("CARGO_PKG_VERSION")
        },
        "agentCapabilities": initialize_agent_capabilities(),
        "authMethods": [],
        "_meta": {
            "sdd": {
                "agent": "sdd-orchestrator",
                "runtimeAgnostic": true,
                "artifactStoreCanonical": true
            }
        }
    })
}

fn initialize_agent_capabilities() -> Value {
    json!({
        "loadSession": true,
        "promptCapabilities": {
            "image": false,
            "audio": false,
            "embeddedContext": false
        },
        "mcpCapabilities": {
            "http": false,
            "sse": false
        },
        "sessionCapabilities": {},
        "auth": {}
    })
}

fn create_session(root: &Path, id: Value, params: &Value) -> Result<Vec<Value>> {
    let cwd = required_abs_path(params, "cwd")?;
    let session_id = new_session_id(&cwd);
    let now = now();
    let session = AcpSession {
        session_id: session_id.clone(),
        cwd,
        mcp_servers: redacted_mcp_servers(params),
        orchestration_slug: None,
        trace_ids: Vec::new(),
        transcript: Vec::new(),
        cancelled: false,
        created_at: now.clone(),
        updated_at: now,
    };
    save_session(root, &session)?;
    Ok(vec![jsonrpc_result(id, json!({ "sessionId": session_id }))])
}

fn load_session(root: &Path, id: Value, params: &Value) -> Result<Vec<Value>> {
    let session_id = required_string(params, "sessionId")?;
    let cwd = required_abs_path(params, "cwd")?;
    let mut session = read_session(root, &session_id)?;
    session.cwd = cwd;
    if let Some(servers) = params.get("mcpServers").and_then(Value::as_array) {
        session.mcp_servers = redact_mcp_server_array(servers);
    }
    session.updated_at = now();
    save_session(root, &session)?;

    let mut responses = Vec::new();
    for entry in &session.transcript {
        responses.push(session_update_message(
            &session.session_id,
            &entry.message_id,
            if entry.role == "user" {
                "user_message_chunk"
            } else {
                "agent_message_chunk"
            },
            &entry.text,
        ));
    }
    responses.push(jsonrpc_result(id, Value::Null));
    Ok(responses)
}

fn prompt_session(root: &Path, id: Value, params: &Value) -> Result<Vec<Value>> {
    let session_id = required_string(params, "sessionId")?;
    let mut session = read_session(root, &session_id)?;
    let prompt = extract_prompt_text(params)?;
    let redacted_prompt = runtime::redaction::redact_text(&prompt);
    let orchestration_slug = params
        .get("orchestration")
        .and_then(Value::as_str)
        .map(crate::artifact_slug)
        .or_else(|| Some(crate::artifact_slug(&redacted_prompt)));
    session.orchestration_slug = orchestration_slug;
    session.cancelled = false;

    let user_message_id = message_id("user", &redacted_prompt);
    session.transcript.push(AcpTranscriptEntry {
        role: "user".to_string(),
        message_id: user_message_id.clone(),
        text: redacted_prompt.clone(),
        at: now(),
    });

    let response_text = build_agent_response(&session, &redacted_prompt);
    let agent_message_id = message_id("agent", &response_text);
    session.transcript.push(AcpTranscriptEntry {
        role: "agent".to_string(),
        message_id: agent_message_id.clone(),
        text: response_text.clone(),
        at: now(),
    });
    session.updated_at = now();
    save_session(root, &session)?;

    Ok(vec![
        session_update_plan(&session.session_id),
        session_update_message(
            &session.session_id,
            &agent_message_id,
            "agent_message_chunk",
            &response_text,
        ),
        jsonrpc_result(id, json!({ "stopReason": "end_turn" })),
    ])
}

fn cancel_session(root: &Path, params: &Value) -> Result<()> {
    let session_id = required_string(params, "sessionId")?;
    let mut session = read_session(root, &session_id)?;
    session.cancelled = true;
    session.updated_at = now();
    save_session(root, &session)
}

fn build_agent_response(session: &AcpSession, prompt: &str) -> String {
    let slug = session
        .orchestration_slug
        .as_deref()
        .unwrap_or("sdd-orchestration");
    format!(
        "Recebi a demanda para o agente `sdd-orchestrator`.\n\nPróximo caminho seguro:\n\n```bash\nsdd init \"{slug}\"\nsdd workflow run agentic-sdd-loop --input {:?} --max-iterations 3 --json\n```\n\nVou tratar ACP como superfície de sessão e manter o artifact store em `docs/{slug}/` como fonte canônica. Nenhuma escrita de código ou aprovação de checkpoint foi executada por este turno ACP.",
        prompt
    )
}

fn session_update_plan(session_id: &str) -> Value {
    json!({
        "jsonrpc": "2.0",
        "method": "session/update",
        "params": {
            "sessionId": session_id,
            "update": {
                "sessionUpdate": "plan",
                "entries": [
                    { "content": "Resolver demanda para um ciclo SDD rastreável", "priority": "high", "status": "completed" },
                    { "content": "Usar artifact store local como fonte canônica", "priority": "high", "status": "pending" },
                    { "content": "Parar em checkpoints humanos", "priority": "high", "status": "pending" }
                ]
            }
        }
    })
}

fn session_update_message(
    session_id: &str,
    message_id: &str,
    session_update: &str,
    text: &str,
) -> Value {
    json!({
        "jsonrpc": "2.0",
        "method": "session/update",
        "params": {
            "sessionId": session_id,
            "update": {
                "sessionUpdate": session_update,
                "messageId": message_id,
                "content": {
                    "type": "text",
                    "text": text
                }
            }
        }
    })
}

fn extract_prompt_text(params: &Value) -> Result<String> {
    if let Some(text) = params.get("prompt").and_then(Value::as_str) {
        return Ok(text.to_string());
    }
    if let Some(text) = params.get("message").and_then(Value::as_str) {
        return Ok(text.to_string());
    }
    if let Some(prompt) = params.get("prompt").and_then(Value::as_array) {
        let mut parts = Vec::new();
        for block in prompt {
            if let Some(text) = block.get("text").and_then(Value::as_str) {
                parts.push(text.to_string());
                continue;
            }
            if let Some(text) = block
                .get("resource")
                .and_then(|resource| resource.get("text"))
                .and_then(Value::as_str)
            {
                parts.push(text.to_string());
                continue;
            }
            if let Some(link) = resource_link_summary(block) {
                parts.push(link);
            }
        }
        if !parts.is_empty() {
            return Ok(parts.join("\n\n"));
        }
    }
    bail!("session/prompt requires a text prompt")
}

fn resource_link_summary(block: &Value) -> Option<String> {
    let resource_link = if block.get("type").and_then(Value::as_str) == Some("resource_link") {
        Some(block)
    } else {
        block
            .get("resourceLink")
            .or_else(|| block.get("resource_link"))
    }?;
    let uri = resource_link.get("uri").and_then(Value::as_str)?;
    let name = resource_link
        .get("name")
        .and_then(Value::as_str)
        .or_else(|| resource_link.get("title").and_then(Value::as_str))
        .unwrap_or("resource");
    let mime_type = resource_link
        .get("mimeType")
        .and_then(Value::as_str)
        .or_else(|| resource_link.get("mime_type").and_then(Value::as_str));
    Some(match mime_type {
        Some(mime_type) => format!("ResourceLink: {name} ({mime_type}) <{uri}>"),
        None => format!("ResourceLink: {name} <{uri}>"),
    })
}

fn redacted_mcp_servers(params: &Value) -> Vec<Value> {
    params
        .get("mcpServers")
        .and_then(Value::as_array)
        .map(|servers| redact_mcp_server_array(servers))
        .unwrap_or_default()
}

fn redact_mcp_server_array(servers: &[Value]) -> Vec<Value> {
    servers.iter().map(redact_mcp_server_value).collect()
}

fn redact_mcp_server_value(value: &Value) -> Value {
    match value {
        Value::Array(items) => Value::Array(items.iter().map(redact_mcp_server_value).collect()),
        Value::Object(map) => {
            let mut redacted = serde_json::Map::new();
            for (key, value) in map {
                if is_sensitive_mcp_value_key(key) {
                    redacted.insert(key.clone(), Value::String(REDACTED.to_string()));
                } else {
                    redacted.insert(key.clone(), redact_mcp_server_value(value));
                }
            }
            Value::Object(redacted)
        }
        Value::String(text) => Value::String(runtime::redaction::redact_text(text)),
        other => other.clone(),
    }
}

fn is_sensitive_mcp_value_key(key: &str) -> bool {
    matches!(
        key.to_ascii_lowercase().as_str(),
        "value" | "authorization" | "token" | "secret" | "apikey" | "api_key" | "password"
    )
}

fn required_abs_path(params: &Value, key: &str) -> Result<PathBuf> {
    let value = required_string(params, key)?;
    let path = PathBuf::from(&value);
    if !path.is_absolute() {
        bail!("{key} must be an absolute path");
    }
    Ok(path)
}

fn required_string(params: &Value, key: &str) -> Result<String> {
    params
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .ok_or_else(|| anyhow!("missing required field `{key}`"))
}

fn read_session(root: &Path, session_id: &str) -> Result<AcpSession> {
    let path = session_path(root, session_id)?;
    let text = fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
    Ok(serde_json::from_str(&text)?)
}

fn save_session(root: &Path, session: &AcpSession) -> Result<()> {
    let path = session_path(root, &session.session_id)?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(
        path,
        format!("{}\n", serde_json::to_string_pretty(session)?),
    )?;
    Ok(())
}

fn session_path(root: &Path, session_id: &str) -> Result<PathBuf> {
    if session_id.contains('/') || session_id.contains('\\') || session_id.contains("..") {
        bail!("invalid ACP session id `{session_id}`");
    }
    Ok(sessions_dir(root).join(format!("{session_id}.json")))
}

fn sessions_dir(root: &Path) -> PathBuf {
    root.join(".sdd").join("acp").join("sessions")
}

fn new_session_id(cwd: &Path) -> String {
    let counter = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
    let seed = format!(
        "{}:{}:{}:{}",
        now(),
        std::process::id(),
        counter,
        cwd.display()
    );
    format!("sess_{}", short_hash(&seed))
}

fn message_id(role: &str, text: &str) -> String {
    format!("msg_{role}_{}", short_hash(text))
}

fn short_hash(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    let digest = hasher.finalize();
    digest[..8]
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect()
}

fn jsonrpc_result(id: Value, result: Value) -> Value {
    json!({ "jsonrpc": "2.0", "id": id, "result": result })
}

fn jsonrpc_error(id: Value, code: i32, message: &str) -> Value {
    json!({
        "jsonrpc": "2.0",
        "id": id,
        "error": { "code": code, "message": message }
    })
}

fn parse_targets(targets: &str) -> Result<Vec<String>> {
    let mut parsed = Vec::new();
    for raw in targets.split(',') {
        let item = raw.trim().to_ascii_lowercase();
        if item.is_empty() {
            continue;
        }
        match item.as_str() {
            "all" | "zed" => parsed.push(item),
            other => bail!("unsupported ACP config target `{other}` (use zed|all)"),
        }
    }
    if parsed.is_empty() {
        parsed.push("all".to_string());
    }
    Ok(parsed)
}

fn render_zed_acp_settings(root: &Path) -> String {
    let root = runtime::platform::display_path(root).replace('\\', "/");
    serde_json::to_string_pretty(&json!({
        "agent_servers": {
            "sdd-layer": {
                "command": "sdd",
                "args": ["acp", "serve", "--root", root],
                "env": {}
            }
        }
    }))
    .expect("ACP settings JSON must serialize")
}

fn now() -> String {
    Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}

#[cfg(feature = "acp-agent")]
fn acp_sdk_marker() -> &'static str {
    std::any::type_name::<agent_client_protocol::Client>()
}

#[cfg(not(feature = "acp-agent"))]
fn acp_sdk_marker() -> &'static str {
    "manual-jsonrpc-stdio (compile with acp-agent for agent-client-protocol runtime crate)"
}

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

    #[test]
    fn serves_initialize_and_session_prompt() {
        let root = tempdir().unwrap();
        let cwd = root.path().to_string_lossy();
        let input = format!(
            "{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{{\"protocolVersion\":1}}}}\n{{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/new\",\"params\":{{\"cwd\":\"{cwd}\",\"mcpServers\":[]}}}}\n"
        );
        let mut output = Vec::new();
        serve_lines(root.path(), input.as_bytes(), &mut output).unwrap();
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("\"protocolVersion\":1"));
        assert!(text.contains("\"sessionId\""));
    }

    #[test]
    fn rejects_relative_cwd() {
        let root = tempdir().unwrap();
        let input = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"session/new\",\"params\":{\"cwd\":\"relative\",\"mcpServers\":[]}}\n";
        let mut output = Vec::new();
        serve_lines(root.path(), &input[..], &mut output).unwrap();
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("\"error\""));
        assert!(text.contains("absolute path"));
    }

    #[test]
    fn creates_unique_session_ids_for_rapid_new_sessions() {
        let root = tempdir().unwrap();
        let cwd = root.path().to_string_lossy();
        let first = create_session(
            root.path(),
            json!(1),
            &json!({ "cwd": cwd, "mcpServers": [] }),
        )
        .unwrap();
        let second = create_session(
            root.path(),
            json!(2),
            &json!({ "cwd": cwd, "mcpServers": [] }),
        )
        .unwrap();
        let first_id = first[0]["result"]["sessionId"].as_str().unwrap();
        let second_id = second[0]["result"]["sessionId"].as_str().unwrap();
        assert_ne!(first_id, second_id);
        assert!(session_path(root.path(), first_id).unwrap().is_file());
        assert!(session_path(root.path(), second_id).unwrap().is_file());
    }

    #[test]
    fn config_supports_zed_dry_run() {
        let root = tempdir().unwrap();
        let actions = config_actions(root.path(), "zed", false, true).unwrap();
        assert_eq!(actions.len(), 1);
        assert!(actions[0].contains("dry-run write"));
    }

    #[test]
    fn prompt_persists_redacted_transcript_and_stop_reason() {
        let root = tempdir().unwrap();
        let cwd = root.path().to_string_lossy();
        let responses = create_session(
            root.path(),
            json!(1),
            &json!({ "cwd": cwd, "mcpServers": [{ "name": "sdd" }] }),
        )
        .unwrap();
        let session_id = responses[0]["result"]["sessionId"]
            .as_str()
            .unwrap()
            .to_string();

        let prompt =
            "implementar feature com OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz1234567890";
        let responses = prompt_session(
            root.path(),
            json!(2),
            &json!({
                "sessionId": session_id,
                "prompt": [{ "type": "text", "text": prompt }]
            }),
        )
        .unwrap();
        assert_eq!(
            responses.last().unwrap()["result"]["stopReason"],
            "end_turn"
        );

        let session = read_session(root.path(), &session_id).unwrap();
        let transcript = serde_json::to_string(&session.transcript).unwrap();
        assert!(!transcript.contains("sk-abcdefghijklmnopqrstuvwxyz1234567890"));
        assert!(transcript.contains("[REDACTED]"));
    }

    #[test]
    fn session_persistence_redacts_mcp_server_secrets() {
        let root = tempdir().unwrap();
        let cwd = root.path().to_string_lossy();
        let responses = create_session(
            root.path(),
            json!(1),
            &json!({
                "cwd": cwd,
                "mcpServers": [{
                    "type": "stdio",
                    "name": "private-tools",
                    "command": "mcp-server",
                    "args": ["--token", "sk-abcdefghijklmnopqrstuvwxyz1234567890"],
                    "env": [
                        { "name": "OPENAI_API_KEY", "value": "plain-secret-value" }
                    ],
                    "headers": [
                        { "name": "Authorization", "value": "Bearer raw-header-secret" }
                    ]
                }]
            }),
        )
        .unwrap();
        let session_id = responses[0]["result"]["sessionId"].as_str().unwrap();
        let session = read_session(root.path(), session_id).unwrap();
        let persisted = serde_json::to_string(&session.mcp_servers).unwrap();
        assert!(persisted.contains("OPENAI_API_KEY"));
        assert!(persisted.contains("Authorization"));
        assert!(persisted.contains("[REDACTED]"));
        assert!(!persisted.contains("plain-secret-value"));
        assert!(!persisted.contains("raw-header-secret"));
        assert!(!persisted.contains("sk-abcdefghijklmnopqrstuvwxyz1234567890"));

        load_session(
            root.path(),
            json!(2),
            &json!({
                "sessionId": session_id,
                "cwd": cwd,
                "mcpServers": [{
                    "type": "stdio",
                    "name": "replacement",
                    "command": "mcp-server",
                    "args": [],
                    "env": [
                        { "name": "ANTHROPIC_API_KEY", "value": "second-secret-value" }
                    ]
                }]
            }),
        )
        .unwrap();
        let session = read_session(root.path(), session_id).unwrap();
        let persisted = serde_json::to_string(&session.mcp_servers).unwrap();
        assert!(persisted.contains("ANTHROPIC_API_KEY"));
        assert!(persisted.contains("[REDACTED]"));
        assert!(!persisted.contains("second-secret-value"));
    }

    #[test]
    fn prompt_accepts_resource_link_content_blocks() {
        let root = tempdir().unwrap();
        let cwd = root.path().to_string_lossy();
        let responses = create_session(
            root.path(),
            json!(1),
            &json!({ "cwd": cwd, "mcpServers": [] }),
        )
        .unwrap();
        let session_id = responses[0]["result"]["sessionId"]
            .as_str()
            .unwrap()
            .to_string();

        let responses = prompt_session(
            root.path(),
            json!(2),
            &json!({
                "sessionId": session_id,
                "prompt": [{
                    "type": "resource_link",
                    "uri": "sdd://agents/sdd-orchestrator",
                    "name": "SDD orchestrator manifest",
                    "mimeType": "application/json"
                }]
            }),
        )
        .unwrap();
        assert_eq!(
            responses.last().unwrap()["result"]["stopReason"],
            "end_turn"
        );

        let session_id = responses[1]["params"]["sessionId"].as_str().unwrap();
        let session = read_session(root.path(), session_id).unwrap();
        let transcript = serde_json::to_string(&session.transcript).unwrap();
        assert!(transcript.contains("ResourceLink: SDD orchestrator manifest"));
        assert!(transcript.contains("sdd://agents/sdd-orchestrator"));
    }
}
