//! Admin API endpoints.
//!
//! Provides endpoints for:
//! - Hot-reloading schema without restart
//! - Invalidating cache by scope (all, entity type, or pattern)
//! - Inspecting runtime configuration (sanitized)

use std::{collections::HashMap, fs};

use axum::{Json, extract::State};
use fraiseql_core::{db::traits::DatabaseAdapter, schema::CompiledSchema};
use serde::{Deserialize, Serialize};
use tracing::{error, info};

use crate::routes::{
    api::types::{ApiError, ApiResponse},
    graphql::AppState,
};

/// Current status of the query result cache as understood by the server.
///
/// Used in the admin config endpoint and startup logs to give operators
/// an accurate picture of what `cache_enabled` actually activates.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CacheStatus {
    /// `cache_enabled = false` — no cache guard or caching active.
    Disabled,
    /// `cache_enabled = true` — RLS safety guard is active, but full
    /// query result caching (`CachedDatabaseAdapter`) is not yet wired.
    #[deprecated(
        since = "2.2.0",
        note = "CachedDatabaseAdapter is now always wired when cache_enabled = true. \
                Use `Active` or `Disabled` instead."
    )]
    RlsGuardOnly,
    /// Full query result caching is active.
    ///
    /// `CachedDatabaseAdapter` is wired into the server when `cache_enabled = true`.
    Active,
}

impl CacheStatus {
    /// Derive cache status from the `cache_enabled` flag.
    ///
    /// # Deprecated
    ///
    /// Use `AppState::adapter_cache_enabled` to determine the true cache state.
    #[must_use]
    #[deprecated(
        since = "2.2.0",
        note = "Use `AppState::adapter_cache_enabled` to determine the true cache state. \
                This function returns `RlsGuardOnly` which is no longer accurate."
    )]
    pub const fn from_cache_enabled(cache_enabled: bool) -> Self {
        #[allow(deprecated)] // Reason: function itself is deprecated; returns deprecated variant
        if cache_enabled {
            Self::RlsGuardOnly
        } else {
            Self::Disabled
        }
    }
}

/// Request to reload schema from file.
#[derive(Debug, Deserialize, Serialize)]
pub struct ReloadSchemaRequest {
    /// Path to compiled schema file
    pub schema_path:   String,
    /// If true, only validate the schema without applying changes
    pub validate_only: bool,
}

/// Response after schema reload attempt.
#[derive(Debug, Serialize)]
pub struct ReloadSchemaResponse {
    /// Whether the operation succeeded
    pub success: bool,
    /// Human-readable message about the result
    pub message: String,
}

/// Request to clear cache entries.
#[derive(Debug, Deserialize, Serialize)]
pub struct CacheClearRequest {
    /// Scope for clearing: "all", "entity", or "pattern"
    pub scope:       String,
    /// Entity type (required if scope is "entity")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub entity_type: Option<String>,
    /// Pattern (required if scope is "pattern")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pattern:     Option<String>,
}

/// Response after cache clear operation.
#[derive(Debug, Serialize)]
pub struct CacheClearResponse {
    /// Whether the operation succeeded
    pub success:         bool,
    /// Number of entries cleared
    pub entries_cleared: usize,
    /// Human-readable message about the result
    pub message:         String,
}

/// Response containing runtime configuration (sanitized).
#[derive(Debug, Serialize)]
pub struct AdminConfigResponse {
    /// Server version
    pub version: String,
    /// Runtime configuration (secrets redacted)
    pub config:  HashMap<String, String>,
}

/// Validate that a caller-supplied schema path is safe to open.
///
/// Two threats are guarded against:
///
/// 1. **Path traversal** — any component equal to `..` would let an attacker escape the intended
///    directory, so such paths are rejected.
/// 2. **Absolute-path escape** — when an `allowed_base` is given, the resolved path must start with
///    that prefix.  An absolute path like `/etc/passwd` is rejected when the allowed base is
///    `/var/fraiseql`.
///
/// # Errors
///
/// Returns `ApiError` with a validation error when the path is unsafe.
pub fn validate_schema_path(
    path: &str,
    allowed_base: Option<&std::path::Path>,
) -> Result<(), ApiError> {
    use std::path::{Component, Path};

    let p = Path::new(path);

    // Reject any `..` component regardless of position.
    if p.components().any(|c| c == Component::ParentDir) {
        return Err(ApiError::validation_error(
            "schema_path must not contain '..' (path traversal rejected)",
        ));
    }

    // When an allowed base is configured, verify the path stays within it.
    if let Some(base) = allowed_base {
        // Build the candidate path: if relative, join onto base; if absolute keep as-is.
        let candidate = if p.is_absolute() {
            p.to_path_buf()
        } else {
            base.join(p)
        };

        // Use `starts_with` on the lexically resolved path.  We intentionally
        // avoid `canonicalize` here to keep the function pure (no I/O), accepting
        // that symlink escapes are a separate, deployment-level concern.
        if !candidate.starts_with(base) {
            return Err(ApiError::validation_error(
                "schema_path is outside the allowed base directory",
            ));
        }
    }

    Ok(())
}

/// Reload schema from file.
///
/// Supports validation-only mode via `validate_only` flag.
/// When applied, the schema is atomically swapped without stopping execution.
///
/// # Errors
///
/// Returns `ApiError` with a validation error if `schema_path` is empty.
/// Returns `ApiError` with a validation error if `schema_path` contains path traversal.
/// Returns `ApiError` with a parse error if the schema file cannot be read or parsed.
///
/// Requires admin token authentication.
pub async fn reload_schema_handler<A: DatabaseAdapter>(
    State(state): State<AppState<A>>,
    Json(req): Json<ReloadSchemaRequest>,
) -> Result<Json<ApiResponse<ReloadSchemaResponse>>, ApiError> {
    let _ = &state; // used conditionally by #[cfg(feature = "arrow")]
    if req.schema_path.is_empty() {
        return Err(ApiError::validation_error("schema_path cannot be empty"));
    }

    // SECURITY: Reject path traversal and out-of-base absolute paths.
    validate_schema_path(&req.schema_path, None)?;

    // Step 1: Load schema from file
    let schema_json = fs::read_to_string(&req.schema_path)
        .map_err(|e| ApiError::parse_error(format!("Failed to read schema file: {}", e)))?;

    // Step 2: Validate schema structure
    let _validated_schema = CompiledSchema::from_json(&schema_json, false)
        .map_err(|e| ApiError::parse_error(format!("Invalid schema JSON: {}", e)))?;

    if req.validate_only {
        info!(
            operation = "admin.reload_schema",
            schema_path = %req.schema_path,
            validate_only = true,
            success = true,
            "Admin: schema validation requested"
        );
        let response = ReloadSchemaResponse {
            success: true,
            message: "Schema validated successfully (not applied)".to_string(),
        };
        Ok(Json(ApiResponse {
            status: "success".to_string(),
            data:   response,
        }))
    } else {
        // Step 3: Atomically swap the executor with the validated schema.
        // We pass the already-validated JSON bytes to avoid re-reading from disk
        // (prevents TOCTOU: the file could change between validation and reload).
        let start = std::time::Instant::now();

        match state.reload_schema_from_json(&schema_json).await {
            Ok(()) => {
                let duration_ms = start.elapsed().as_millis();
                state
                    .metrics
                    .schema_reloads_total
                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                info!(
                    operation = "admin.reload_schema",
                    schema_path = %req.schema_path,
                    duration_ms,
                    "Schema reloaded successfully"
                );

                let response = ReloadSchemaResponse {
                    success: true,
                    message: format!("Schema reloaded from {} in {duration_ms}ms", req.schema_path),
                };
                Ok(Json(ApiResponse {
                    status: "success".to_string(),
                    data:   response,
                }))
            },
            Err(e) => {
                state
                    .metrics
                    .schema_reload_errors_total
                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                error!(
                    operation = "admin.reload_schema",
                    schema_path = %req.schema_path,
                    error = %e,
                    "Schema reload failed"
                );
                Err(ApiError::internal_error(format!("Schema reload failed: {e}")))
            },
        }
    }
}

/// Cache statistics response.
#[derive(Debug, Serialize)]
pub struct CacheStatsResponse {
    /// Number of entries currently in cache
    pub entries_count: usize,
    /// Whether cache is enabled
    pub cache_enabled: bool,
    /// Cache TTL in seconds
    pub ttl_secs:      u64,
    /// Human-readable message
    pub message:       String,
}

/// Clear cache entries by scope.
///
/// Supports three clearing scopes:
/// - **all**: Clear all cache entries
/// - **entity**: Clear entries for a specific entity type
/// - **pattern**: Clear entries matching a glob pattern
///
/// # Errors
///
/// Returns `ApiError` with an internal error if the cache feature is not enabled.
/// Returns `ApiError` with a validation error if required parameters are missing or scope is
/// invalid.
///
/// Requires admin token authentication.
pub async fn cache_clear_handler<A: DatabaseAdapter>(
    State(state): State<AppState<A>>,
    Json(req): Json<CacheClearRequest>,
) -> Result<Json<ApiResponse<CacheClearResponse>>, ApiError> {
    // Cache operations require the `arrow` feature.
    #[cfg(not(feature = "arrow"))]
    {
        let _ = (state, req);
        Err(ApiError::internal_error("Cache not configured"))
    }

    #[cfg(feature = "arrow")]
    // Validate scope and required parameters
    match req.scope.as_str() {
        "all" => {
            if let Some(cache) = state.cache() {
                let entries_before = cache.len();
                cache.clear();
                info!(
                    operation = "admin.cache_clear",
                    scope = "all",
                    entries_cleared = entries_before,
                    success = true,
                    "Admin: cache cleared (all entries)"
                );
                let response = CacheClearResponse {
                    success:         true,
                    entries_cleared: entries_before,
                    message:         format!("Cleared {} cache entries", entries_before),
                };
                Ok(Json(ApiResponse {
                    status: "success".to_string(),
                    data:   response,
                }))
            } else {
                Err(ApiError::internal_error("Cache not configured"))
            }
        },
        "entity" => {
            if req.entity_type.is_none() {
                return Err(ApiError::validation_error(
                    "entity_type is required when scope is 'entity'",
                ));
            }

            if let Some(cache) = state.cache() {
                let entity_type = req.entity_type.as_ref().ok_or_else(|| {
                    ApiError::internal_error(
                        "entity_type was None after validation — this is a bug",
                    )
                })?;
                // Convert entity type to view name pattern (e.g., User → v_user)
                let view_name = format!("v_{}", entity_type.to_lowercase());
                let entries_cleared = cache.invalidate_views(&[&view_name]);
                info!(
                    operation = "admin.cache_clear",
                    scope = "entity",
                    entity_type = %entity_type,
                    entries_cleared,
                    success = true,
                    "Admin: cache cleared for entity"
                );
                let response = CacheClearResponse {
                    success: true,
                    entries_cleared,
                    message: format!(
                        "Cleared {} cache entries for entity type '{}'",
                        entries_cleared, entity_type
                    ),
                };
                Ok(Json(ApiResponse {
                    status: "success".to_string(),
                    data:   response,
                }))
            } else {
                Err(ApiError::internal_error("Cache not configured"))
            }
        },
        "pattern" => {
            if req.pattern.is_none() {
                return Err(ApiError::validation_error(
                    "pattern is required when scope is 'pattern'",
                ));
            }

            if let Some(cache) = state.cache() {
                let pattern = req.pattern.as_ref().ok_or_else(|| {
                    ApiError::internal_error("pattern was None after validation — this is a bug")
                })?;
                let entries_cleared = cache.invalidate_pattern(pattern);
                info!(
                    operation = "admin.cache_clear",
                    scope = "pattern",
                    %pattern,
                    entries_cleared,
                    success = true,
                    "Admin: cache cleared by pattern"
                );
                let response = CacheClearResponse {
                    success: true,
                    entries_cleared,
                    message: format!(
                        "Cleared {} cache entries matching pattern '{}'",
                        entries_cleared, pattern
                    ),
                };
                Ok(Json(ApiResponse {
                    status: "success".to_string(),
                    data:   response,
                }))
            } else {
                Err(ApiError::internal_error("Cache not configured"))
            }
        },
        _ => Err(ApiError::validation_error("scope must be 'all', 'entity', or 'pattern'")),
    }
}

/// Get cache statistics.
///
/// Returns current cache metrics including entry count, enabled status, and TTL.
///
/// # Errors
///
/// This handler currently always succeeds; it is infallible.
///
/// Requires admin token authentication.
pub async fn cache_stats_handler<A: DatabaseAdapter>(
    State(state): State<AppState<A>>,
) -> Result<Json<ApiResponse<CacheStatsResponse>>, ApiError> {
    #[cfg(feature = "arrow")]
    if let Some(cache) = state.cache() {
        let response = CacheStatsResponse {
            entries_count: cache.len(),
            cache_enabled: true,
            ttl_secs:      60, // Default TTL from QueryCache::new(60)
            message:       format!("Cache contains {} entries with 60-second TTL", cache.len()),
        };
        return Ok(Json(ApiResponse {
            status: "success".to_string(),
            data:   response,
        }));
    }
    {
        let _ = state;
        let response = CacheStatsResponse {
            entries_count: 0,
            cache_enabled: false,
            ttl_secs:      0,
            message:       "Cache is not configured".to_string(),
        };
        Ok(Json(ApiResponse {
            status: "success".to_string(),
            data:   response,
        }))
    }
}

/// Get sanitized runtime configuration.
///
/// Returns server version and runtime configuration with secrets redacted.
/// Configuration includes database settings, cache settings, etc.
/// but excludes API keys, passwords, and other sensitive data.
///
/// # Errors
///
/// This handler currently always succeeds; it is infallible.
///
/// Requires admin token authentication.
// Reason: `cache_enabled = "false"` appears in both the else-branch and the
// `#[cfg(not(feature = "arrow"))]` inner path. Clippy sees them as shared code, but
// extracting it would break the `#[cfg]` conditional logic that sets a different value
// when `arrow` is enabled.
#[allow(clippy::branches_sharing_code)] // Reason: branches are logically distinct; extracting shared code would obscure intent
pub async fn config_handler<A: DatabaseAdapter>(
    State(state): State<AppState<A>>,
) -> Result<Json<ApiResponse<AdminConfigResponse>>, ApiError> {
    let mut config = HashMap::new();

    // Get actual server configuration
    if let Some(server_config) = state.server_config() {
        // Safe configuration values - no secrets
        config.insert("port".to_string(), server_config.port.to_string());
        config.insert("host".to_string(), server_config.host.clone());

        if let Some(workers) = server_config.workers {
            config.insert("workers".to_string(), workers.to_string());
        }

        // TLS status (boolean only, paths are redacted)
        config.insert("tls_enabled".to_string(), server_config.tls.is_some().to_string());

        // Request limits
        if let Some(limits) = &server_config.limits {
            config.insert("max_request_size".to_string(), limits.max_request_size.clone());
            config.insert("request_timeout".to_string(), limits.request_timeout.clone());
            config.insert(
                "max_concurrent_requests".to_string(),
                limits.max_concurrent_requests.to_string(),
            );
            config.insert("max_queue_depth".to_string(), limits.max_queue_depth.to_string());
        }

        // Cache status: read from adapter_cache_enabled (set at startup by ServerBuilder).
        // This reflects the CachedDatabaseAdapter state, independent of the Arrow cache.
        let cache_active = state.adapter_cache_enabled;

        config.insert("cache_enabled".to_string(), cache_active.to_string());
        let cache_status = if cache_active {
            CacheStatus::Active
        } else {
            CacheStatus::Disabled
        };
        config.insert(
            "cache_status".to_string(),
            serde_json::to_string(&cache_status)
                .unwrap_or_else(|_| "\"disabled\"".to_string())
                .trim_matches('"')
                .to_string(),
        );
        let _ = server_config; // consumed above for other fields
    } else {
        // Minimal configuration if not available
        config.insert("cache_enabled".to_string(), "false".to_string());
        config.insert("cache_status".to_string(), "disabled".to_string());
    }

    let response = AdminConfigResponse {
        version: env!("CARGO_PKG_VERSION").to_string(),
        config,
    };

    Ok(Json(ApiResponse {
        status: "success".to_string(),
        data:   response,
    }))
}

/// Request body for `POST /api/v1/admin/explain`.
#[derive(Debug, Deserialize, Serialize)]
pub struct ExplainRequest {
    /// Name of the regular query to explain (e.g., `"users"`).
    pub query: String,

    /// GraphQL-style variable filters passed as a JSON object.
    ///
    /// Each key-value pair becomes an equality condition in the WHERE clause.
    /// Example: `{"status": "active"}` → `WHERE data->>'status' = 'active'`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub variables: Option<serde_json::Value>,

    /// Optional row limit to pass to the query.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u32>,

    /// Optional row offset to pass to the query.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offset: Option<u32>,
}

/// Return the pre-built Grafana dashboard JSON for FraiseQL metrics.
///
/// The dashboard JSON is embedded at compile time from
/// `deploy/grafana/fraiseql-dashboard.json`.  Operators can import it into
/// Grafana with a single `curl` command (see `deploy/grafana/README.md`).
///
/// # Errors
///
/// This handler is infallible — the embedded JSON is validated at compile time
/// by the `test_grafana_dashboard_is_valid_json` unit test.
///
/// Requires admin token authentication.
pub async fn grafana_dashboard_handler<A: DatabaseAdapter>(
    State(_state): State<AppState<A>>,
) -> impl axum::response::IntoResponse {
    const DASHBOARD_JSON: &str = include_str!("../../../resources/fraiseql-dashboard.json");

    (
        axum::http::StatusCode::OK,
        [(axum::http::header::CONTENT_TYPE, "application/json")],
        DASHBOARD_JSON,
    )
}

/// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` for a named query.
///
/// Accepts a query name and optional variable filters, then executes
/// `EXPLAIN ANALYZE` against the backing PostgreSQL view using the exact
/// same parameterized SQL that a live query would use.
///
/// # Errors
///
/// * `400 Bad Request` — empty query name, unknown query, or mutation given
/// * `500 Internal Server Error` — database execution failure
///
/// Requires admin token authentication.
pub async fn explain_handler<A: DatabaseAdapter + 'static>(
    State(state): State<AppState<A>>,
    Json(req): Json<ExplainRequest>,
) -> Result<Json<ApiResponse<fraiseql_core::runtime::ExplainResult>>, ApiError> {
    if req.query.is_empty() {
        return Err(ApiError::validation_error("query cannot be empty"));
    }

    state
        .executor()
        .explain(&req.query, req.variables.as_ref(), req.limit, req.offset)
        .await
        .map(ApiResponse::success)
        .map_err(|e| match e {
            fraiseql_core::error::FraiseQLError::Validation { message, .. } => {
                ApiError::validation_error(message)
            },
            fraiseql_core::error::FraiseQLError::Unsupported { message } => {
                ApiError::validation_error(format!("Unsupported: {message}"))
            },
            other => ApiError::internal_error(other.to_string()),
        })
}
