//! ToolInvoker app-tauri pour l'adapter OpenAI-compatible.
//!
//! C'est la porte locale qui donne aux modèles HTTP la même surface `idea_*` que
//! le serveur MCP : même catalogue, même mapping en `OrchestratorCommand`, même
//! `OrchestratorService::dispatch`.

use std::sync::{Arc, Mutex};

use application::OrchestratorService;
use async_trait::async_trait;
use domain::ports::{ProjectStore, ToolInvocationError, ToolInvoker, ToolSpec};
use serde_json::Value;

const PROJECT_ROOT_ARG: &str = "__ideaProjectRoot";
const REQUESTER_ARG: &str = "__ideaRequester";

/// Invoker d'outils OpenAI-compatible branché sur l'orchestrateur applicatif.
pub struct AppOpenAiToolInvoker {
    orchestrator: Arc<OrchestratorService>,
    projects: Arc<dyn ProjectStore>,
}

/// Proxy injecté avant que l'orchestrateur soit construit, puis lié dans la
/// composition root. Il casse uniquement le cycle de wiring, pas le contrat runtime.
#[derive(Default)]
pub struct LateBoundOpenAiToolInvoker {
    inner: Mutex<Option<Arc<dyn ToolInvoker>>>,
}

impl LateBoundOpenAiToolInvoker {
    /// Construit un proxy vide.
    #[must_use]
    pub fn new() -> Self {
        Self {
            inner: Mutex::new(None),
        }
    }

    /// Lie l'implémentation réelle. Appelé une fois par la composition root.
    pub fn bind(&self, inner: Arc<dyn ToolInvoker>) {
        *self.inner.lock().expect("mutex sain") = Some(inner);
    }
}

#[async_trait]
impl ToolInvoker for LateBoundOpenAiToolInvoker {
    fn tools(&self) -> Vec<ToolSpec> {
        self.inner
            .lock()
            .expect("mutex sain")
            .as_ref()
            .map_or_else(Vec::new, |inner| inner.tools())
    }

    async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
        let inner = self
            .inner
            .lock()
            .expect("mutex sain")
            .clone()
            .ok_or_else(|| {
                ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned())
            })?;
        inner.call(name, args_json).await
    }
}

impl AppOpenAiToolInvoker {
    /// Construit l'invoker depuis le service orchestrateur et le store projet.
    #[must_use]
    pub fn new(orchestrator: Arc<OrchestratorService>, projects: Arc<dyn ProjectStore>) -> Self {
        Self {
            orchestrator,
            projects,
        }
    }
}

#[async_trait]
impl ToolInvoker for AppOpenAiToolInvoker {
    fn tools(&self) -> Vec<ToolSpec> {
        infrastructure::orchestrator::mcp::catalogue()
            .into_iter()
            .map(|tool| ToolSpec {
                name: tool.name.to_owned(),
                description: tool.description.to_owned(),
                input_schema: tool.input_schema,
            })
            .collect()
    }

    async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
        let value: Value = serde_json::from_str(args_json)
            .map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
        let args = value.as_object().ok_or_else(|| {
            ToolInvocationError::InvalidArguments(
                "les arguments d'outil doivent être un objet JSON".to_owned(),
            )
        })?;
        let project_root = args
            .get(PROJECT_ROOT_ARG)
            .and_then(Value::as_str)
            .ok_or_else(|| {
                ToolInvocationError::InvalidArguments(
                    "contexte projet interne absent pour l'outil".to_owned(),
                )
            })?;
        let requester = args
            .get(REQUESTER_ARG)
            .and_then(Value::as_str)
            .ok_or_else(|| {
                ToolInvocationError::InvalidArguments(
                    "identité requester interne absente pour l'outil".to_owned(),
                )
            })?;
        let project = self
            .projects
            .list_projects()
            .await
            .map_err(|e| ToolInvocationError::Execution(e.to_string()))?
            .into_iter()
            .find(|project| project.root.as_str() == project_root)
            .ok_or_else(|| {
                ToolInvocationError::Execution(format!(
                    "projet introuvable pour root `{project_root}`"
                ))
            })?;
        let command = infrastructure::orchestrator::mcp::map_tool_call(name, &value, requester)
            .map_err(|e| match e {
                infrastructure::orchestrator::mcp::ToolMapError::UnknownTool(tool) => {
                    ToolInvocationError::NotFound(tool)
                }
                infrastructure::orchestrator::mcp::ToolMapError::BadArguments(tool) => {
                    ToolInvocationError::InvalidArguments(format!(
                        "arguments invalides pour `{tool}`"
                    ))
                }
                infrastructure::orchestrator::mcp::ToolMapError::Invalid(err) => {
                    ToolInvocationError::InvalidArguments(err.to_string())
                }
            })?;
        let outcome = self
            .orchestrator
            .dispatch(&project, command)
            .await
            .map_err(|e| ToolInvocationError::Execution(e.to_string()))?;
        Ok(outcome.reply.unwrap_or(outcome.detail))
    }
}
