//! Claude native-agent backend: drives the installed `claude` CLI in headless
//! print mode (uses the user's subscription auth). We stream `stream-json`
//! events so the build is observable in real time, and constrain it to file
//! tools.

use super::{
    binary_on_path, compose_native_prompt, run_streaming, AuthStatus, Provider, ProviderKind,
};
use crate::agent::{AgentOutcome, AgentTask};
use crate::config::ProviderId;
use crate::progress::{Event, Progress};
use anyhow::{bail, Result};
use async_trait::async_trait;
use serde_json::Value;
use std::path::Path;

pub struct ClaudeProvider {
    model: Option<String>,
}

impl ClaudeProvider {
    pub fn new(model: Option<String>) -> ClaudeProvider {
        ClaudeProvider { model }
    }
}

#[async_trait]
impl Provider for ClaudeProvider {
    fn id(&self) -> ProviderId {
        ProviderId::Claude
    }
    fn kind(&self) -> ProviderKind {
        ProviderKind::NativeAgent
    }

    async fn run_agent(&self, task: &AgentTask, progress: &dyn Progress) -> Result<AgentOutcome> {
        let tools = if task.read_only {
            "Read Glob Grep LS"
        } else {
            "Read Edit Write Glob Grep LS"
        };

        let mut args: Vec<String> = vec![
            "-p".into(),
            "--append-system-prompt".into(),
            task.system.clone(),
            "--output-format".into(),
            "stream-json".into(),
            "--verbose".into(), // required with stream-json in -p mode
            "--allowed-tools".into(),
            tools.into(),
            "--add-dir".into(),
            task.wiki_dir.display().to_string(),
        ];
        if !task.read_only {
            args.push("--permission-mode".into());
            args.push("acceptEdits".into());
        }
        if let Some(m) = task.model.as_ref().or(self.model.as_ref()) {
            args.push("--model".into());
            args.push(m.clone());
        }

        let prompt = compose_native_prompt(task);
        let repo_root = task.repo_root.clone();
        let wiki_name = task
            .wiki_dir
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("wiki")
            .to_string();

        let mut result_text = String::new();
        let mut is_error = false;
        let mut cost: Option<f64> = None;
        let mut turns = 0usize;

        let (_full, stderr, status) =
            run_streaming("claude", &args, &task.repo_root, &prompt, |line| {
                let v: Value = match serde_json::from_str(line) {
                    Ok(v) => v,
                    Err(_) => return,
                };
                match v.get("type").and_then(|t| t.as_str()) {
                    Some("assistant") => {
                        if let Some(content) = v
                            .get("message")
                            .and_then(|m| m.get("content"))
                            .and_then(|c| c.as_array())
                        {
                            for block in content {
                                match block.get("type").and_then(|t| t.as_str()) {
                                    Some("text") => {
                                        if let Some(t) = block.get("text").and_then(|x| x.as_str())
                                        {
                                            progress.event(Event::note(t));
                                        }
                                    }
                                    Some("tool_use") => {
                                        let name = block
                                            .get("name")
                                            .and_then(|x| x.as_str())
                                            .unwrap_or("");
                                        let input = block.get("input").cloned().unwrap_or_default();
                                        if matches!(name, "Write" | "Edit") {
                                            progress.event(Event::page(page_path(
                                                &input, &repo_root, &wiki_name,
                                            )));
                                        } else {
                                            progress.event(Event::tool(
                                                name,
                                                target(&input, &repo_root),
                                            ));
                                        }
                                    }
                                    _ => {}
                                }
                            }
                        }
                    }
                    Some("result") => {
                        is_error = v.get("is_error").and_then(|b| b.as_bool()).unwrap_or(false);
                        result_text = v
                            .get("result")
                            .and_then(|r| r.as_str())
                            .unwrap_or("")
                            .to_string();
                        cost = v.get("total_cost_usd").and_then(|c| c.as_f64());
                        turns = v.get("num_turns").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
                    }
                    _ => {}
                }
            })
            .await?;

        if is_error {
            bail!(
                "claude reported an error: {}",
                crate::util::truncate_chars(result_text.trim(), 600)
            );
        }
        if !status.success() && result_text.is_empty() {
            bail!(
                "claude exited with failure: {}",
                crate::util::truncate_chars(stderr.trim(), 600)
            );
        }

        Ok(AgentOutcome {
            final_text: result_text,
            tool_calls: turns,
            cost_usd: cost,
        })
    }

    async fn auth_status(&self) -> AuthStatus {
        if binary_on_path("claude") {
            AuthStatus::Ok
        } else {
            AuthStatus::NeedsLogin
        }
    }

    async fn list_models(&self) -> Result<Vec<String>> {
        Ok(vec![
            "claude-opus-4-8".to_string(),
            "claude-sonnet-4-6".to_string(),
            "claude-haiku-4-5".to_string(),
        ])
    }
}

/// Path relative to the repo root (falls back to the raw string).
fn rel_to_repo(p: &str, repo_root: &Path) -> String {
    match Path::new(p).strip_prefix(repo_root) {
        Ok(s) => s.to_string_lossy().replace('\\', "/"),
        Err(_) => p.trim_start_matches("./").to_string(),
    }
}

/// Wiki-relative display path for a Write/Edit on a wiki file.
fn page_path(input: &Value, repo_root: &Path, wiki_name: &str) -> String {
    let fp = input
        .get("file_path")
        .or_else(|| input.get("path"))
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let rel = rel_to_repo(fp, repo_root);
    rel.strip_prefix(&format!("{wiki_name}/"))
        .map(|s| s.to_string())
        .unwrap_or(rel)
}

/// Short target for a non-write tool call (path or search pattern).
fn target(input: &Value, repo_root: &Path) -> String {
    if let Some(p) = input
        .get("file_path")
        .or_else(|| input.get("path"))
        .and_then(|v| v.as_str())
    {
        return rel_to_repo(p, repo_root);
    }
    input
        .get("pattern")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string()
}
