//! @docs ARCHITECTURE:Agent
//!
//! ### AI Assist Note
//! **Model Manager (LLM Orchestrator)**: Orchestrates the AI provider
//! configurations and model lifecycle for the Tadpole OS engine.
//! Features **IMR-01 (Intelligent Model Registry)**: provides automated
//! discovery and synchronization of models from remote providers (Ollama,
//! OpenAI, Groq, etc.).
//! Features **Provider Handshake Validation**: provides a
//! connectivity test mechanism (`test_provider`) to verify API keys
//! and endpoint reactivity across diverse protocols.
//! Implements **Secure Secret Redaction**: ensures that `api_key`
//! values are stripped from frontend responses while maintaining
//! functional status indicators (`has_api_key`).
//!
//! ### 🔍 Debugging & Observability
//! - **Failure Path**: 401 Unauthorized during provider handshakes
//!   due to malformed keys, 400 Bad Request for insecure HTTP
//!   transmissions to external endpoints (SEC-01), or dependency
//!   orphaning if models are not decommissioned alongside providers.
//! - **Telemetry Link**: Search for `🔍 [Test Trace]` or `🗑️ [infra-trace]`
//!   in `tracing` logs for infrastructure audit events.
//! - **Trace Scope**: `server-rs::routes::model_manager`

use crate::{
    agent::{
        capability_matrix::CapabilityMatrix,
        types::{ModelEntry, ProviderConfig, Validatable},
    },
    error::AppError,
    state::AppState,
};
use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use std::sync::Arc;

#[derive(serde::Serialize)]
pub struct ProviderResponse {
    pub id: String,
    pub name: String,
    pub icon: Option<String>,
    pub base_url: Option<String>,
    pub protocol: crate::agent::types::ModelProvider,
    pub external_id: Option<String>,
    pub supports_steering_vectors: bool,
    pub audio_model: Option<String>,
    pub api_key: Option<String>,
    pub has_api_key: bool,
}

impl From<&ProviderConfig> for ProviderResponse {
    fn from(config: &ProviderConfig) -> Self {
        let has_key = config
            .api_key
            .as_ref()
            .is_some_and(|s| !s.trim().is_empty());
        Self {
            id: config.id.clone(),
            name: config.name.clone(),
            icon: config.icon.clone(),
            base_url: config.base_url.clone(),
            protocol: config.protocol,
            external_id: config.external_id.clone(),
            supports_steering_vectors: config.supports_steering_vectors,
            audio_model: config.audio_model.clone(),
            api_key: None,
            has_api_key: has_key,
        }
    }
}

pub async fn get_ollama_host() -> String {
    let host = std::env::var("OLLAMA_HOST").unwrap_or_default();
    if host.is_empty() {
        crate::networking::resolver::AddressResolver::resolve_local_url(11434).await
    } else {
        crate::networking::resolver::AddressResolver::resolve_url_if_local(&host).await
    }
}

/// SEC-01: Validates that a URL is either local/private or uses HTTPS.
/// Prevents accidental transmission of API keys over unencrypted channels to external providers.
fn validate_url_security(url: &str) -> Result<(), AppError> {
    let secure_local = url.contains("localhost")
        || url.contains("127.0.0.1")
        || url.contains("host.docker.internal")
        || url.starts_with("http://192.168.")
        || url.starts_with("http://10.");

    let allow_insecure = std::env::var("TADPOLE_ALLOW_LOCAL_HTTP").is_ok();

    if url.starts_with("http://") && !secure_local && !allow_insecure {
        return Err(AppError::BadRequest(
            "Insecure transmission blocked: API keys cannot be sent over HTTP to external providers. Use HTTPS."
                .to_string(),
        ));
    }
    Ok(())
}

enum ProtocolRouter {
    Anthropic,
    Google,
    OpenAI,
    Ollama,
}

impl ProtocolRouter {
    fn from_protocol(protocol: &str) -> Self {
        match protocol {
            "anthropic" => Self::Anthropic,
            "google" | "gemini" => Self::Google,
            "ollama" => Self::Ollama,
            _ => Self::OpenAI, // Default for standard open-compatible endpoints
        }
    }

    async fn build_models_url(&self, protocol: &str, base_url: &str, is_sync: bool) -> String {
        match self {
            Self::Anthropic => {
                let base = if base_url.is_empty() {
                    "https://api.anthropic.com".to_string()
                } else {
                    crate::networking::resolver::AddressResolver::resolve_url_if_local(base_url)
                        .await
                };
                format!("{}/v1/models", base.trim_end_matches('/'))
            }
            Self::Google => {
                let base = if base_url.is_empty() {
                    "https://generativelanguage.googleapis.com".to_string()
                } else {
                    crate::networking::resolver::AddressResolver::resolve_url_if_local(base_url)
                        .await
                };
                let mut base = base.trim_end_matches('/').to_string();
                if is_sync {
                    format!("{}/v1/models", base)
                } else {
                    if !base.contains("/v1") && !base.contains("/beta") {
                        base.push_str("/v1");
                    }
                    format!("{}/models", base)
                }
            }
            Self::Ollama => {
                let base = if base_url.is_empty() {
                    get_ollama_host().await
                } else {
                    crate::networking::resolver::AddressResolver::resolve_url_if_local(base_url)
                        .await
                };
                let base = if !base.contains("/api") && !base.contains("/v1") {
                    format!("{}/api", base)
                } else {
                    base
                };
                if is_sync && !base.contains("/v1") {
                    format!("{}/tags", base)
                } else {
                    format!("{}/models", base)
                }
            }
            Self::OpenAI => {
                let base = if base_url.is_empty() {
                    match protocol {
                        "groq" => "https://api.groq.com/openai/v1".to_string(),
                        _ => "https://api.openai.com/v1".to_string(),
                    }
                } else {
                    crate::networking::resolver::AddressResolver::resolve_url_if_local(base_url)
                        .await
                };
                format!("{}/models", base.trim_end_matches('/'))
            }
        }
    }

    fn authenticate_request(
        &self,
        req: reqwest::RequestBuilder,
        api_key: &str,
    ) -> reqwest::RequestBuilder {
        match self {
            Self::Google => req.header("x-goog-api-key", api_key),
            Self::Anthropic => req
                .header("x-api-key", api_key)
                .header("anthropic-version", "2023-06-01"),
            Self::Ollama => {
                if !api_key.is_empty() {
                    req.header("Authorization", format!("Bearer {}", api_key))
                } else {
                    req
                }
            }
            Self::OpenAI => req.header("Authorization", format!("Bearer {}", api_key)),
        }
    }

    fn parse_discovery_response(&self, body: &serde_json::Value, url: &str) -> Vec<String> {
        let mut discovered_ids = Vec::new();
        match self {
            Self::Ollama if url.contains("/tags") => {
                if let Some(models) = body.get("models").and_then(|m| m.as_array()) {
                    for m in models {
                        if let Some(name) = m.get("name").and_then(|n| n.as_str()) {
                            discovered_ids.push(name.to_string());
                        }
                    }
                }
            }
            Self::Google => {
                if let Some(models) = body.get("models").and_then(|m| m.as_array()) {
                    for m in models {
                        if let Some(name) = m.get("name").and_then(|n| n.as_str()) {
                            discovered_ids.push(name.replace("models/", ""));
                        }
                    }
                }
            }
            _ => {
                // OpenAI standard
                if let Some(data) = body.get("data").and_then(|d| d.as_array()) {
                    for m in data {
                        if let Some(id) = m.get("id").and_then(|i| i.as_str()) {
                            discovered_ids.push(id.to_string());
                        }
                    }
                }
            }
        }
        discovered_ids
    }
}

/// Returns all configured AI providers.
/// SEC-02: Strips `api_key` from the response so secrets are never sent to the frontend.
#[tracing::instrument(skip(state), fields(trace_id = tracing::field::Empty, request_id = tracing::field::Empty, http_status = tracing::field::Empty), name = "infra_providers::list")]
pub async fn get_providers(
    State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
    let sanitized: Vec<ProviderResponse> = state
        .registry
        .providers
        .iter()
        .map(|kv| ProviderResponse::from(kv.value()))
        .collect();
    Ok(Json(sanitized))
}

/// Updates or creates a provider configuration.
#[tracing::instrument(skip(state, config), fields(provider_id = %id), name = "infra_providers::update")]
pub async fn update_provider(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    Json(config): Json<ProviderConfig>,
) -> Result<impl IntoResponse, AppError> {
    if let Err(e) = config.validate() {
        return Err(AppError::BadRequest(e));
    }
    state.registry.providers.insert(id.clone(), config);
    state.save_providers().await;
    Ok((
        StatusCode::OK,
        Json(serde_json::json!({ "status": "updated", "id": id })),
    ))
}

/// Deletes a provider configuration.
#[tracing::instrument(skip(state), fields(provider_id = %id, trace_id = tracing::field::Empty, request_id = tracing::field::Empty, http_status = tracing::field::Empty), name = "infra_providers::delete")]
pub async fn delete_provider(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
    tracing::info!("🗑️ [infra-trace] Deleting provider: {}", id);
    let removed = state.registry.providers.remove(&id);
    if removed.is_some() {
        tracing::info!("✅ [infra-trace] Successfully removed provider: {}", id);

        // Also remove all models associated with this provider
        let initial_count = state.registry.models.len();
        state
            .registry
            .models
            .retain(|_, model| model.provider_id != id);
        let removed_count = initial_count - state.registry.models.len();
        if removed_count > 0 {
            tracing::info!(
                "🗑️ [infra-trace] Decommissioned {} associated models for provider: {}",
                removed_count,
                id
            );
        }

        // Save both registries
        state.save_providers().await;
        state.save_models().await;

        Ok((
            StatusCode::OK,
            Json(serde_json::json!({ "status": "deleted", "id": id })),
        ))
    } else {
        tracing::warn!("⚠️ [infra-trace] Provider not found for deletion: {}", id);
        Err(AppError::NotFound(format!("Provider '{}' not found", id)))
    }
}

/// Returns all available agent models.
#[tracing::instrument(skip(state), fields(trace_id = tracing::field::Empty, request_id = tracing::field::Empty, http_status = tracing::field::Empty), name = "infra_models::list")]
pub async fn get_models(State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, AppError> {
    let models: Vec<ModelEntry> = state
        .registry
        .models
        .iter()
        .map(|kv| {
            let mut entry = kv.value().clone();
            // Enrich with capabilities if they are empty/default
            if entry.capabilities.context_window == 0 {
                entry.capabilities = CapabilityMatrix::infer_capabilities(&entry.id);
            }
            entry
        })
        .collect();
    Ok(Json(models))
}

/// Updates or creates a model entry.
#[tracing::instrument(skip(state, entry), fields(model_id = %id), name = "infra_models::update")]
pub async fn update_model(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    Json(entry): Json<ModelEntry>,
) -> Result<impl IntoResponse, AppError> {
    if let Err(e) = entry.validate() {
        return Err(AppError::BadRequest(e));
    }

    // Verify provider existence for swarm consistency
    if !state.registry.providers.contains_key(&entry.provider_id) {
        return Err(AppError::BadRequest(format!(
            "Provider '{}' is not registered in the neural infrastructure.",
            entry.provider_id
        )));
    }

    let mut entry = entry;
    // Enrich with baseline capabilities if not provided
    if entry.capabilities.context_window == 0 {
        entry.capabilities = CapabilityMatrix::infer_capabilities(&id);
    }

    state.registry.models.insert(id.clone(), entry);
    state.save_models().await;
    Ok((
        StatusCode::OK,
        Json(serde_json::json!({ "status": "updated", "id": id })),
    ))
}

/// Deletes a model from the registry.
#[tracing::instrument(skip(state), fields(model_id = %id, trace_id = tracing::field::Empty, request_id = tracing::field::Empty, http_status = tracing::field::Empty), name = "infra_models::delete")]
pub async fn delete_model(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
    tracing::info!("🗑️ [infra-trace] Deleting model: {}", id);
    let removed = state.registry.models.remove(&id);
    if removed.is_some() {
        tracing::info!("✅ [infra-trace] Successfully removed model: {}", id);
        state.save_models().await;
        Ok((
            StatusCode::OK,
            Json(serde_json::json!({ "status": "deleted", "id": id })),
        ))
    } else {
        tracing::warn!("⚠️ [infra-trace] Model not found for deletion: {}", id);
        Err(AppError::NotFound(format!("Model '{}' not found", id)))
    }
}

/// Tests a provider configuration by performing a dummy model list or balance check.
#[tracing::instrument(skip(state, config), fields(provider_id = %id), name = "infra_providers::test")]
pub async fn test_provider(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    Json(config): Json<ProviderConfig>,
) -> Result<impl IntoResponse, AppError> {
    tracing::info!("🔍 [Test Trace] Initiating handshake for provider: {}", id);

    let api_key = config
        .api_key
        .filter(|k| !k.is_empty())
        .or_else(|| {
            state
                .registry
                .providers
                .get(&id)
                .and_then(|p| p.api_key.clone())
        })
        .unwrap_or_default();
    let base_url = config.base_url.as_deref().unwrap_or("");
    let protocol = config.protocol.to_string().to_lowercase();

    if api_key.is_empty() {
        return Err(AppError::BadRequest(
            "Missing Secure API Key. handshakes require authentication.".to_string(),
        ));
    }

    let router = ProtocolRouter::from_protocol(&protocol);
    let url = router.build_models_url(&protocol, base_url, false).await;

    // SEC-01: Enforce secure transmission for external handshakes
    validate_url_security(&url)?;

    let request = state.resources.http_client.get(&url);
    let request = router.authenticate_request(request, &api_key);

    match request.send().await {
        Ok(res) => {
            if res.status().is_success() {
                tracing::info!("✅ [Test Trace] Handshake successful for {}", id);
                Ok((
                    StatusCode::OK,
                    Json(serde_json::json!({
                        "status": "success",
                        "message": format!("Handshake successful: {} endpoint is reactive.", config.name)
                    })),
                ))
            } else {
                let err_text = res
                    .text()
                    .await
                    .unwrap_or_else(|_| "Unknown error".to_string());
                let safe_err = state.security.secret_redactor.redact(&err_text);
                tracing::warn!("❌ [Test Trace] Handshake failed for {}: {}", id, safe_err);
                Err(AppError::Unauthorized(format!(
                    "Handshake failed: {}",
                    safe_err
                )))
            }
        }
        Err(e) => {
            let safe_err = state.security.secret_redactor.redact(&format!("{}", e));
            tracing::error!("🚨 [Test Trace] Network error for {}: {}", id, safe_err);
            Err(AppError::InternalServerError(format!(
                "Network Error: {}",
                safe_err
            )))
        }
    }
}

#[derive(serde::Deserialize)]
pub struct SyncModelsPayload {
    pub api_key: Option<String>,
}

/// Dynamic synchronization of models from a provider's discovery endpoint.
/// Part of IMR-01.
#[tracing::instrument(skip(state, payload), fields(provider_id = %id), name = "infra_providers::sync")]
pub async fn sync_provider_models(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    Json(payload): Json<SyncModelsPayload>,
) -> Result<impl IntoResponse, AppError> {
    tracing::info!("🔄 [IMR] Synchronizing models for provider: {}", id);

    let provider = state
        .registry
        .providers
        .get(&id)
        .ok_or_else(|| AppError::NotFound(format!("Provider '{}' not found", id)))?;

    let api_key = payload
        .api_key
        .filter(|k| !k.is_empty())
        .or_else(|| provider.api_key.clone())
        .unwrap_or_default();
    let base_url = provider.base_url.as_deref().unwrap_or("");
    let protocol = provider.protocol.to_string().to_lowercase();

    // 1. Resolve URL and Authenticate via Router
    let router = ProtocolRouter::from_protocol(&protocol);
    let url = router.build_models_url(&protocol, base_url, true).await;

    // SEC-01: Enforce secure transmission for external discovery
    validate_url_security(&url)?;

    let request = state.resources.http_client.get(&url);
    let request = router.authenticate_request(request, &api_key);

    let resp = request.send().await.map_err(|e| {
        AppError::InternalServerError(format!("Handshake failed during discovery: {}", e))
    })?;

    if !resp.status().is_success() {
        return Err(AppError::Unauthorized(format!(
            "Discovery failed ({}): Provider rejected credentials or endpoint unreachable.",
            resp.status()
        )));
    }

    let body: serde_json::Value = resp.json().await.map_err(|_| {
        AppError::InternalServerError("Failed to parse discovery response.".to_string())
    })?;

    // 2. Parse Model IDs based on protocol
    let discovered_ids = router.parse_discovery_response(&body, &url);

    let count = discovered_ids.len();
    let mut added = 0;

    // 3. Register and Enrich
    for model_id in discovered_ids {
        let entry_id = format!("{}:{}", id, model_id.replace("/", "-"));
        if !state.registry.models.contains_key(&entry_id) {
            let capabilities = CapabilityMatrix::infer_capabilities(&model_id);
            let entry = ModelEntry {
                id: model_id.clone(),
                name: model_id.clone(),
                provider_id: id.clone(),
                provider: Some(provider.protocol),
                modality: if capabilities.supports_vision {
                    crate::agent::types::Modality::Vision
                } else {
                    crate::agent::types::Modality::Llm
                },
                capabilities,
                ..ModelEntry::default()
            };
            state.registry.models.insert(entry_id, entry);
            added += 1;
        }
    }

    if added > 0 {
        state.save_models().await;
    }

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "status": "success",
            "message": format!("Synchronized {} models ({} total discovered).", added, count),
            "added": added,
            "discovered": count
        })),
    ))
}

/// Returns a curated catalog of recommended models for the local swarm.
pub async fn get_model_catalog(
    State(_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
    let catalog = serde_json::json!([
        {
            "id": "llama3:8b",
            "name": "Llama 3 (8B)",
            "provider": "ollama",
            "description": "Meta's most capable 8B model. Balanced for logic and creative tasks.",
            "size": "4.7GB",
            "vram": "8GB",
            "tags": ["General", "Logic"]
        },
        {
            "id": "phi3:latest",
            "name": "Phi-3 Mini",
            "provider": "ollama",
            "description": "Microsoft's high-performance 3.8B small language model.",
            "size": "2.3GB",
            "vram": "4GB",
            "tags": ["Fast", "Efficiency"]
        },
        {
            "id": "mistral:latest",
            "name": "Mistral (7B)",
            "provider": "ollama",
            "description": "The original sovereign open-weight champion.",
            "size": "4.1GB",
            "vram": "6GB",
            "tags": ["Original", "Balanced"]
        },
        {
            "id": "nomic-embed-text:latest",
            "name": "Nomic Embed",
            "provider": "ollama",
            "description": "High-fidelity text embeddings for RAG and Vector Memory.",
            "size": "274MB",
            "vram": "1GB",
            "tags": ["RAG", "Embeddings"]
        }
    ]);

    Ok(Json(catalog))
}

#[derive(serde::Deserialize)]
pub struct PullModelPayload {
    pub node_id: String,
    pub tag: String,
}

/// Proxies a model pull request to a specific swarm node.
pub async fn pull_model(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<PullModelPayload>,
) -> Result<impl IntoResponse, AppError> {
    tracing::info!(
        "📥 [ModelStore] Requesting pull of {} on node {}",
        payload.tag,
        payload.node_id
    );

    // 1. Locate the node
    let node = state.registry.nodes.get(&payload.node_id).ok_or_else(|| {
        AppError::NotFound(format!("Node '{}' not found in swarm", payload.node_id))
    })?;

    // 2. Prepare the Ollama pull request
    // We hit the Tadpole Engine's unified proxy on the target node.
    let ollama_url = format!("http://{}/v1/api/pull", node.address);
    let ollama_payload = serde_json::json!({
        "name": payload.tag,
        "stream": false
    });

    // 3. Dispatch the request with Swarm Authentication (NEURAL_TOKEN)
    let resp = state
        .resources
        .http_client
        .post(&ollama_url)
        .header(
            "Authorization",
            format!("Bearer {}", state.security.deploy_token),
        )
        .json(&ollama_payload)
        .send()
        .await
        .map_err(|e| {
            AppError::InternalServerError(format!("Failed to reach Bunker node: {}", e))
        })?;

    if resp.status().is_success() {
        tracing::info!(
            "✅ [ModelStore] Successfully initiated pull on {}",
            node.name
        );
        Ok((
            StatusCode::OK,
            Json(
                serde_json::json!({ "status": "success", "message": format!("Model {} is being pulled to {}", payload.tag, node.name) }),
            ),
        ))
    } else {
        let status = resp.status();
        let err = resp
            .text()
            .await
            .unwrap_or_else(|_| "Unknown error".to_string());
        tracing::error!(
            "❌ [ModelStore] Pull failed on {} ({}): {}",
            node.name,
            status,
            err
        );
        Err(AppError::InternalServerError(format!(
            "Bunker node '{}' reported error ({}): {}",
            node.name, status, err
        )))
    }
}

/// A lightweight proxy handler that forwards local model pull requests
/// from the Engine port (8000) to the local Ollama instance (11434).
/// This ensures a unified "Swarm Gateway" for all node operations.
pub async fn ollama_proxy_pull(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<serde_json::Value>,
) -> Result<impl IntoResponse, AppError> {
    let ollama_host = get_ollama_host().await;
    let ollama_url = format!("{}/api/pull", ollama_host);

    tracing::info!(
        "🔄 [ModelStore] Proxying pull request to local Ollama: {}",
        ollama_url
    );

    let resp = state
        .resources
        .http_client
        .post(&ollama_url)
        .json(&payload)
        .send()
        .await
        .map_err(|e| {
            AppError::InternalServerError(format!("Failed to reach local Ollama: {}", e))
        })?;

    let status = resp.status();
    let body = resp.text().await.unwrap_or_else(|_| "{}".to_string());

    if status.is_success() {
        Ok((status, body))
    } else {
        tracing::error!(
            "❌ [ModelStore] Local Ollama pull failed ({}): {}",
            status,
            body
        );
        Err(AppError::InternalServerError(format!(
            "Ollama reported error: {}",
            body
        )))
    }
}

// Metadata: [model_manager]
