//! OpenAI-compatible backend (raw-model): OpenAI, OpenRouter, and Ollama all
//! speak `/chat/completions` with function/tool calling — one client, three
//! providers, distinguished by base URL and a [`Flavor`].

use super::{AuthStatus, Provider, ProviderKind};
use crate::agent::runner::{self, ChatModel, ModelStep, Msg, ToolCall};
use crate::agent::tools::{ToolCtx, ToolSchema};
use crate::agent::{AgentOutcome, AgentTask};
use crate::config::ProviderId;
use crate::progress::Progress;
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use serde_json::{json, Value};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Flavor {
    OpenAi,
    OpenRouter,
    Ollama,
}

pub struct OpenAiCompat {
    client: reqwest::Client,
    flavor: Flavor,
    base_url: String,
    api_key: Option<String>,
    model: Option<String>,
}

impl OpenAiCompat {
    pub fn new(
        flavor: Flavor,
        base_url: String,
        api_key: Option<String>,
        model: Option<String>,
    ) -> OpenAiCompat {
        OpenAiCompat {
            client: super::http_client(),
            flavor,
            base_url,
            api_key,
            model,
        }
    }

    fn provider_id(&self) -> ProviderId {
        match self.flavor {
            Flavor::OpenAi => ProviderId::Openai,
            Flavor::OpenRouter => ProviderId::Openrouter,
            Flavor::Ollama => ProviderId::Ollama,
        }
    }

    fn model(&self) -> Result<&str> {
        self.model
            .as_deref()
            .ok_or_else(|| anyhow!("no model set — pass --model or set it in config"))
    }

    fn auth_header(&self, mut req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        if let Some(key) = &self.api_key {
            req = req.header("authorization", format!("Bearer {key}"));
        }
        if self.flavor == Flavor::OpenRouter {
            req = req
                .header("http-referer", "https://github.com/")
                .header("x-title", "hephaes");
        }
        req
    }
}

fn to_messages(system: &str, messages: &[Msg]) -> Vec<Value> {
    let mut out = vec![json!({"role":"system","content":system})];
    for m in messages {
        match m {
            Msg::User(text) => out.push(json!({"role":"user","content":text.clone()})),
            Msg::Assistant { text, tool_calls } => {
                let calls: Vec<Value> = tool_calls
                    .iter()
                    .map(|tc| {
                        json!({
                            "id":tc.id.clone(),
                            "type":"function",
                            "function":{
                                "name":tc.name.clone(),
                                "arguments": tc.input.to_string()
                            }
                        })
                    })
                    .collect();
                let mut msg = json!({
                    "role":"assistant",
                    "content": text.clone().unwrap_or_default(),
                });
                if !calls.is_empty() {
                    msg["tool_calls"] = json!(calls);
                }
                out.push(msg);
            }
            Msg::ToolResults(results) => {
                for r in results {
                    out.push(json!({
                        "role":"tool",
                        "tool_call_id":r.id.clone(),
                        "content":r.content.clone()
                    }));
                }
            }
        }
    }
    out
}

fn to_tools(tools: &[ToolSchema]) -> Vec<Value> {
    tools
        .iter()
        .map(|t| {
            json!({
                "type":"function",
                "function":{
                    "name":t.name,
                    "description":t.description,
                    "parameters":t.parameters
                }
            })
        })
        .collect()
}

#[async_trait]
impl ChatModel for OpenAiCompat {
    async fn step(
        &self,
        system: &str,
        messages: &[Msg],
        tools: &[ToolSchema],
    ) -> Result<ModelStep> {
        let mut body = json!({
            "model": self.model()?,
            "messages": to_messages(system, messages),
            "tools": to_tools(tools),
        });
        // Ollama's OpenAI shim is finickier about tool_choice; omit it there.
        if self.flavor != Flavor::Ollama {
            body["tool_choice"] = json!("auto");
        }

        let req = self
            .client
            .post(format!("{}/chat/completions", self.base_url))
            .header("content-type", "application/json")
            .json(&body);
        let resp = self
            .auth_header(req)
            .send()
            .await
            .context("calling chat/completions")?;

        let status = resp.status();
        let text = resp.text().await.unwrap_or_default();
        if !status.is_success() {
            bail!(
                "API error {status}: {}",
                crate::util::truncate_chars(&text, 600)
            );
        }
        let v: Value = serde_json::from_str(&text).context("parsing completion response")?;

        let msg = v
            .get("choices")
            .and_then(|c| c.as_array())
            .and_then(|a| a.first())
            .and_then(|c| c.get("message"))
            .ok_or_else(|| anyhow!("no choices in response"))?;

        let out_text = msg
            .get("content")
            .and_then(|c| c.as_str())
            .unwrap_or_default()
            .to_string();

        let mut tool_calls = Vec::new();
        if let Some(calls) = msg.get("tool_calls").and_then(|t| t.as_array()) {
            for c in calls {
                let func = c.get("function");
                let name = func
                    .and_then(|f| f.get("name"))
                    .and_then(|n| n.as_str())
                    .unwrap_or_default();
                let id = c.get("id").and_then(|i| i.as_str()).unwrap_or_default();
                // Skip malformed tool calls: an empty tool_call_id echoed back is
                // rejected by the API and breaks the conversation.
                if id.is_empty() || name.is_empty() {
                    continue;
                }
                let args_str = func
                    .and_then(|f| f.get("arguments"))
                    .and_then(|a| a.as_str())
                    .unwrap_or("{}");
                let input = serde_json::from_str(args_str).unwrap_or_else(|_| json!({}));
                tool_calls.push(ToolCall {
                    id: id.to_string(),
                    name: name.to_string(),
                    input,
                });
            }
        }

        Ok(ModelStep {
            text: if out_text.is_empty() {
                None
            } else {
                Some(out_text)
            },
            tool_calls,
        })
    }
}

#[async_trait]
impl Provider for OpenAiCompat {
    fn id(&self) -> ProviderId {
        self.provider_id()
    }
    fn kind(&self) -> ProviderKind {
        ProviderKind::RawModel
    }

    async fn run_agent(&self, task: &AgentTask, progress: &dyn Progress) -> Result<AgentOutcome> {
        self.model()?;
        let ctx = ToolCtx::new(
            &task.repo_root,
            &task.wiki_dir,
            task.repo_map.clone(),
            task.read_only,
        );
        runner::run_loop(
            self,
            &task.system,
            task.prompt(),
            &ctx,
            task.max_iters,
            progress,
        )
        .await
    }

    async fn auth_status(&self) -> AuthStatus {
        match self.flavor {
            Flavor::Ollama => AuthStatus::Ok,
            _ => {
                if self.api_key.is_some() {
                    AuthStatus::Ok
                } else {
                    AuthStatus::MissingKey
                }
            }
        }
    }

    async fn list_models(&self) -> Result<Vec<String>> {
        let req = self.client.get(format!("{}/models", self.base_url));
        let resp = self
            .auth_header(req)
            .send()
            .await
            .context("listing models")?;
        let status = resp.status();
        let text = resp.text().await.unwrap_or_default();
        if !status.is_success() {
            bail!(
                "API error {status}: {}",
                crate::util::truncate_chars(&text, 300)
            );
        }
        let v: Value = serde_json::from_str(&text)?;
        let mut ids: Vec<String> = v
            .get("data")
            .and_then(|d| d.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|m| m.get("id").and_then(|i| i.as_str()).map(String::from))
                    .collect()
            })
            .unwrap_or_default();
        ids.sort();
        Ok(ids)
    }
}
