//! Codex native-agent backend: drives the installed `codex exec` non-interactive
//! runner (uses the user's ChatGPT subscription auth). We stream `--json` events
//! for live progress (best-effort — Codex's event schema is less stable than
//! Claude's) and capture the final message via `-o <file>`.

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;

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

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

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

    async fn run_agent(&self, task: &AgentTask, progress: &dyn Progress) -> Result<AgentOutcome> {
        let sandbox = if task.read_only {
            "read-only"
        } else {
            "workspace-write"
        };
        let tmp_dir = task.repo_root.join(".hephaes");
        let _ = std::fs::create_dir_all(&tmp_dir);
        let last_msg = tmp_dir.join(format!("codex-last-{}.txt", std::process::id()));

        let mut args: Vec<String> = vec![
            "exec".into(),
            "--cd".into(),
            task.repo_root.display().to_string(),
            "-s".into(),
            sandbox.into(),
            "--skip-git-repo-check".into(),
            "--json".into(),
            "--color".into(),
            "never".into(),
            "-o".into(),
            last_msg.display().to_string(),
        ];
        if let Some(m) = task.model.as_ref().or(self.model.as_ref()) {
            args.push("-m".into());
            args.push(m.clone());
        }

        let prompt = format!("{}\n\n{}", task.system, compose_native_prompt(task));
        let (_full, stderr, status) =
            run_streaming("codex", &args, &task.repo_root, &prompt, |line| {
                if let Ok(v) = serde_json::from_str::<Value>(line) {
                    surface_event(progress, &v);
                }
            })
            .await?;

        let final_text = std::fs::read_to_string(&last_msg)
            .ok()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .unwrap_or_default();
        let _ = std::fs::remove_file(&last_msg);

        if !status.success() && final_text.is_empty() {
            bail!(
                "codex exited with failure: {}",
                crate::util::truncate_chars(stderr.trim(), 600)
            );
        }

        Ok(AgentOutcome {
            final_text,
            tool_calls: 0,
            cost_usd: None,
        })
    }

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

    async fn list_models(&self) -> Result<Vec<String>> {
        Ok(vec![
            "gpt-5".to_string(),
            "gpt-5-codex".to_string(),
            "o4-mini".to_string(),
        ])
    }
}

/// Best-effort mapping of a Codex JSON event to a progress event.
fn surface_event(progress: &dyn Progress, v: &Value) {
    if let Some(item) = v.get("item") {
        let kind = item.get("type").and_then(|x| x.as_str()).unwrap_or("");
        match kind {
            "agent_message" | "assistant_message" => {
                if let Some(t) = item
                    .get("text")
                    .or_else(|| item.get("message"))
                    .and_then(|x| x.as_str())
                {
                    progress.event(Event::note(t));
                }
            }
            "file_change" => {
                let path = item.get("path").and_then(|x| x.as_str()).unwrap_or("");
                if path.ends_with(".md") {
                    let rel = path.rsplit("/wiki/").next().unwrap_or(path);
                    progress.event(Event::page(rel));
                } else {
                    progress.event(Event::tool("file_change", path));
                }
            }
            "command_execution" => {
                let cmd = item.get("command").and_then(|x| x.as_str()).unwrap_or("");
                progress.event(Event::tool("command", crate::util::truncate_chars(cmd, 60)));
            }
            other if !other.is_empty() => progress.event(Event::tool(other, "")),
            _ => {}
        }
        return;
    }
    if let Some(t) = v.get("type").and_then(|x| x.as_str()) {
        if t.contains("message") {
            if let Some(x) = v.get("text").and_then(|s| s.as_str()) {
                progress.event(Event::note(x));
            }
        } else if !t.is_empty() {
            progress.event(Event::tool(t, ""));
        }
    }
}
