//! Management REST API for OSTP server.
//!
//! Provides endpoints for third-party panels (like 3x-ui) to manage users,
//! query traffic statistics, and control the server.
//!
//! ## Endpoints
//!
//! - `GET  /api/server/status`       -- Server status (uptime, sessions, version)
//! - `GET  /api/users`               -- List all users with traffic stats
//! - `GET  /api/users/:key`          -- Single user stats
//! - `POST /api/users`               -- Create new access key
//! - `DELETE /api/users/:key`        -- Remove access key
//! - `PUT  /api/users/:key/limit`    -- Set traffic limit for a user
//! - `POST /api/users/:key/reset`    -- Reset user traffic counters

use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::sync::atomic::Ordering;
use portable_atomic::AtomicU64;
use std::time::Instant;

use axum::{
    extract::{Path, State},
    http::{header, StatusCode, Uri},
    response::{IntoResponse},
    routing::{get, post, put},
    Json, Router,
};
use sha2::Digest;
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};

use crate::dispatcher::{UserStats, UserStatsSnapshot};
use crate::outbound::OutboundRule;

// ── Shared state for API handlers ────────────────────────────────────────────

/// API server shared state. Held behind Arc for axum handlers.
#[derive(Clone)]
pub struct ApiState {
    pub access_keys: Arc<RwLock<HashMap<String, UserMeta>>>,
    pub user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
    pub start_time: Instant,
    pub session_token: Arc<RwLock<Option<String>>>,
    pub api_token: Option<String>,
    pub webpath: String,
    pub username: String,
    pub password_hash: String,
    /// Server address for subscription links (e.g. "example.com")
    pub server_host: String,
    pub server_port: u16,
    pub config_path: Option<std::path::PathBuf>,
    pub dns_server: std::sync::Arc<crate::dns::DnsServer>,
    pub audit_logs: Arc<RwLock<Vec<AuditLogEntry>>>,
    pub router: std::sync::Arc<crate::router::Router>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLogEntry {
    pub id: String,
    pub time: String,
    #[serde(rename = "eventEn")]
    pub event_en: String,
    #[serde(rename = "eventRu")]
    pub event_ru: String,
    pub success: bool,
}

#[derive(Deserialize)]
pub struct CreateAuditLogRequest {
    #[serde(rename = "eventEn")]
    pub event_en: String,
    #[serde(rename = "eventRu")]
    pub event_ru: String,
    pub success: bool,
}

// ── API configuration ────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ApiConfig {
    pub enabled: bool,
    pub bind: String,
    pub token: Option<String>,
    #[serde(default)]
    pub webpath: String,
    #[serde(default)]
    pub username: String,
    #[serde(default)]
    pub password_hash: String,
}

impl Default for ApiConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            bind: "127.0.0.1:9090".to_string(),
            token: None,
            webpath: String::new(),
            username: String::new(),
            password_hash: String::new(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserMeta {
    pub name: Option<String>,
    pub limit_bytes: Option<u64>,
}

// ── Request/Response types ───────────────────────────────────────────────────

#[derive(Serialize)]
struct ServerStatus {
    version: &'static str,
    uptime_seconds: u64,
    active_users: usize,
    total_users: usize,
}

#[derive(Deserialize)]
pub struct CreateUserRequest {
    pub access_key: Option<String>,
    pub name: Option<String>,
    pub limit_bytes: Option<u64>,
}

#[derive(Deserialize)]
pub struct UpdateUserRequest {
    pub name: Option<String>,
    pub limit_bytes: Option<u64>,
}

#[derive(Deserialize)]
pub struct SetLimitRequest {
    pub limit_bytes: Option<u64>,
}

#[derive(Deserialize)]
pub struct LoginRequest {
    pub username: String,
    pub password: Option<String>, // We'll accept raw password and hash it
}

#[derive(Serialize)]
pub struct LoginResponse {
    pub token: String,
}

#[derive(Serialize)]
struct ApiResponse<T: Serialize> {
    ok: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    data: Option<T>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

impl<T: Serialize> ApiResponse<T> {
    fn success(data: T) -> Json<Self> {
        Json(Self { ok: true, data: Some(data), error: None })
    }
}

fn api_error<T: Serialize>(msg: &str) -> (StatusCode, Json<ApiResponse<T>>) {
    (StatusCode::BAD_REQUEST, Json(ApiResponse { ok: false, data: None, error: Some(msg.to_string()) }))
}

fn api_unauthorized<T: Serialize>() -> (StatusCode, Json<ApiResponse<T>>) {
    (StatusCode::UNAUTHORIZED, Json(ApiResponse { ok: false, data: None, error: Some("unauthorized".to_string()) }))
}

#[derive(rust_embed::RustEmbed)]
#[folder = "../ostp-control/dist"]
struct Assets;

async fn static_handler(State(state): State<ApiState>, uri: Uri) -> impl IntoResponse {
    let mut path = uri.path().trim_start_matches(&format!("/{}", state.webpath.trim_matches('/'))).to_string();
    if path.starts_with('/') {
        path.remove(0);
    }
    let path = if path.is_empty() { "index.html".to_string() } else { path };

    match Assets::get(&path) {
        Some(content) => {
            let mime = mime_guess::from_path(&path).first_or_octet_stream();
            ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
        }
        None => {
            if path.contains('.') {
                (StatusCode::NOT_FOUND, "404 Not Found").into_response()
            } else {
                match Assets::get("index.html") {
                    Some(content) => ([(header::CONTENT_TYPE, "text/html")], content.data).into_response(),
                    None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
                }
            }
        }
    }
}

// ── API router ───────────────────────────────────────────────────────────────

pub fn create_api_router(state: ApiState) -> Router {
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);

    let api_router = Router::new()
        .route("/server/status", get(handle_status))
        .route("/server/config", get(handle_get_config).put(handle_put_config))
        .route(
            "/users",
            get(handle_list_users).post(handle_create_user),
        )
        .route(
            "/users/{key}",
            get(handle_get_user)
                .put(update_user)
                .delete(delete_user),
        )
        .route("/users/{key}/limit", put(handle_set_limit))
        .route("/users/{key}/reset", post(handle_reset_stats))
        .route("/subscribe/{key}", get(handle_subscribe))
        .route("/login", post(handle_login))
        .route(
            "/audit",
            get(handle_get_audit)
                .post(handle_create_audit)
                .delete(handle_clear_audit),
        )
        .route("/users/bulk", post(handle_bulk_create_users))
        .route("/router/rules", get(handle_get_rules).put(handle_put_rules));

    let webpath = state.webpath.clone();
    let webpath = webpath.trim_matches('/');

    let base_route = if webpath.is_empty() {
        "/panel".to_string()
    } else {
        format!("/{}", webpath)
    };

    let redirect_target = format!("{}/", base_route);
    let redirect_route = base_route.clone();

    Router::new()
        // Exact /{webpath} → redirect to /{webpath}/ (so relative asset paths work)
        .route(&redirect_route, get(move || {
            let target = redirect_target.clone();
            async move { axum::response::Redirect::permanent(&target) }
        }))
        // /{webpath}/ and /{webpath}/** → serve embedded static files
        .route(&format!("{}/", base_route), get(static_handler.clone()))
        .route(&format!("{}/{{*path}}", base_route), get(static_handler))
        // /{webpath}/api/* → API handlers
        .nest(&format!("{}/api", base_route), api_router)
        .layer(cors)
        .with_state(state)
}
/// Start the Management API server on the configured bind address.
pub async fn start_api_server(
    config: ApiConfig,
    access_keys: Arc<RwLock<HashMap<String, UserMeta>>>,
    user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
    server_host: String,
    server_port: u16,
    config_path: Option<std::path::PathBuf>,
    dns_server: std::sync::Arc<crate::dns::DnsServer>,
    router: std::sync::Arc<crate::router::Router>,
) {
    let state = ApiState {
        access_keys,
        user_stats,
        start_time: Instant::now(),
        session_token: Arc::new(RwLock::new(None)),
        api_token: config.token.clone(),
        webpath: config.webpath.clone(),
        username: config.username.clone(),
        password_hash: config.password_hash.clone(),
        server_host,
        server_port,
        config_path,
        dns_server,
        audit_logs: Arc::new(RwLock::new(Vec::new())),
        router,
    };

    let app = create_api_router(state);

    let listener = match tokio::net::TcpListener::bind(&config.bind).await {
        Ok(l) => l,
        Err(e) => {
            tracing::error!("Management API failed to bind on {}: {}", config.bind, e);
            return;
        }
    };

    tracing::info!("Management API listening on {}", config.bind);

    if let Err(e) = axum::serve(listener, app).await {
        tracing::error!("Management API error: {}", e);
    }
}

// ── Middleware: token check ──────────────────────────────────────────────────

fn check_token(state: &ApiState, headers: &axum::http::HeaderMap) -> bool {
    // Both session token (for web UI) and static API token (for relays) are checked
    let mut allowed = false;

    // If no credentials configured, panel is open (unsafe but possible)
    if state.username.is_empty() && state.password_hash.is_empty() && state.api_token.is_none() {
        return true;
    }

    if let Some(value) = headers.get("authorization") {
        if let Ok(val) = value.to_str() {
            if let Some(token) = val.strip_prefix("Bearer ") {
                let current_session = state.session_token.read().unwrap_or_else(|e| e.into_inner()).clone();
                if let Some(session) = current_session {
                    if token == session {
                        allowed = true;
                    }
                }
                
                if let Some(ref api_tok) = state.api_token {
                    if token == api_tok {
                        allowed = true;
                    }
                }
            } else {
                if let Some(ref api_tok) = state.api_token {
                    if val == api_tok {
                        allowed = true;
                    }
                }
            }
        }
    }

    allowed
}

async fn handle_login(
    State(state): State<ApiState>,
    Json(payload): Json<LoginRequest>,
) -> impl IntoResponse {
    if state.username.is_empty() || state.password_hash.is_empty() {
        return api_error("Auth not configured");
    }

    if payload.username != state.username {
        return api_unauthorized::<LoginResponse>();
    }

    let password = payload.password.unwrap_or_default();
    let hash = sha2::Sha256::digest(password.as_bytes());
    let hash_hex = format!("{:x}", hash);

    if hash_hex == state.password_hash {
        let token = uuid::Uuid::new_v4().to_string();
        *state.session_token.write().unwrap_or_else(|e| e.into_inner()) = Some(token.clone());
        (StatusCode::OK, ApiResponse::success(LoginResponse { token }))
    } else {
        api_unauthorized::<LoginResponse>()
    }
}

fn save_config_keys(state: &ApiState) -> Result<(), String> {
    let Some(ref path) = state.config_path else {
        return Ok(());
    };

    let content = std::fs::read_to_string(path)
        .map_err(|e| format!("failed to read config file: {}", e))?;

    let mut stripped = json_comments::StripComments::new(content.as_bytes());
    let mut content_str = String::new();
    use std::io::Read;
    stripped.read_to_string(&mut content_str)
        .map_err(|e| format!("failed to strip comments from config: {}", e))?;

    let mut json_val: serde_json::Value = serde_json::from_str(&content_str)
        .map_err(|e| format!("failed to parse config JSON: {}", e))?;

    let keys = state.access_keys.read().unwrap_or_else(|e| e.into_inner());
    let mut access_keys_json = Vec::new();
    for (k, m) in keys.iter() {
        if m.name.is_none() && m.limit_bytes.is_none() {
            access_keys_json.push(serde_json::Value::String(k.clone()));
        } else {
            let mut obj = serde_json::Map::new();
            obj.insert("access_key".to_string(), serde_json::Value::String(k.clone()));
            if let Some(ref name) = m.name {
                obj.insert("name".to_string(), serde_json::Value::String(name.clone()));
            }
            if let Some(limit) = m.limit_bytes {
                obj.insert("limit_bytes".to_string(), serde_json::Value::Number(limit.into()));
            }
            access_keys_json.push(serde_json::Value::Object(obj));
        }
    }

    if let Some(obj) = json_val.as_object_mut() {
        obj.insert("access_keys".to_string(), serde_json::Value::Array(access_keys_json));
    } else {
        return Err("config root is not an object".to_string());
    }

    let new_content = serde_json::to_string_pretty(&json_val)
        .map_err(|e| format!("failed to serialize config JSON: {}", e))?;

    std::fs::write(path, new_content)
        .map_err(|e| format!("failed to write config file: {}", e))?;

    Ok(())
}


// ── Handlers ─────────────────────────────────────────────────────────────────

async fn handle_get_config(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<serde_json::Value>();
    }

    let Some(ref path) = state.config_path else {
        return api_error("No config path registered (run-time only)");
    };

    match std::fs::read_to_string(path) {
        Ok(content) => {
            let mut stripped = json_comments::StripComments::new(content.as_bytes());
            let mut content_str = String::new();
            use std::io::Read;
            if let Err(e) = stripped.read_to_string(&mut content_str) {
                return api_error(&format!("Failed to strip comments: {}", e));
            }
            match serde_json::from_str::<serde_json::Value>(&content_str) {
                Ok(val) => (StatusCode::OK, ApiResponse::success(val)),
                Err(e) => api_error(&format!("Failed to parse config JSON: {}", e)),
            }
        }
        Err(e) => api_error(&format!("Failed to read config file: {}", e)),
    }
}

async fn handle_put_config(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Json(body): Json<serde_json::Value>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<bool>();
    }

    let Some(ref path) = state.config_path else {
        return api_error("No config path registered (run-time only)");
    };

    if body.get("mode").is_none() {
        return api_error("Invalid config: missing 'mode' field");
    }

    let new_content = match serde_json::to_string_pretty(&body) {
        Ok(s) => s,
        Err(e) => return api_error(&format!("Failed to format config JSON: {}", e)),
    };

    if let Err(e) = std::fs::write(path, new_content) {
        return api_error(&format!("Failed to write config file: {}", e));
    }

    (StatusCode::OK, ApiResponse::success(true))
}

async fn handle_status(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<ServerStatus>();
    }

    let keys = state.access_keys.read().unwrap_or_else(|e| e.into_inner());
    let stats = state.user_stats.read().unwrap_or_else(|e| e.into_inner());
    let online = stats.values()
        .filter(|us| {
            let total = us.bytes_up.load(Ordering::Relaxed) + us.bytes_down.load(Ordering::Relaxed);
            total > 0
        })
        .count();

    let status = ServerStatus {
        version: env!("CARGO_PKG_VERSION"),
        uptime_seconds: state.start_time.elapsed().as_secs(),
        active_users: online,
        total_users: keys.len(),
    };

    (StatusCode::OK, ApiResponse::success(status))
}

async fn handle_list_users(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<Vec<UserStatsSnapshot>>();
    }

    let keys = state.access_keys.read().unwrap_or_else(|e| e.into_inner());
    let stats = state.user_stats.read().unwrap_or_else(|e| e.into_inner());

    let mut users: Vec<UserStatsSnapshot> = keys.iter().map(|(key, meta)| {
        if let Some(st) = stats.get(key) {
            UserStatsSnapshot {
                access_key: key.clone(),
                name: meta.name.clone(),
                bytes_up: st.bytes_up.load(Ordering::Relaxed),
                bytes_down: st.bytes_down.load(Ordering::Relaxed),
                connections: st.connections.load(Ordering::Relaxed),
                limit_bytes: st.limit_bytes,
                online: st.connections.load(Ordering::Relaxed) > 0,
                last_seen: None,
            }
        } else {
            UserStatsSnapshot {
                access_key: key.clone(),
                name: meta.name.clone(),
                bytes_up: 0,
                bytes_down: 0,
                connections: 0,
                limit_bytes: meta.limit_bytes,
                online: false,
                last_seen: None,
            }
        }
    }).collect();

    users.sort_by(|a, b| b.bytes_down.cmp(&a.bytes_down));

    (StatusCode::OK, ApiResponse::success(users))
}

async fn handle_get_user(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Path(key): Path<String>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<UserStatsSnapshot>();
    }

    let keys = state.access_keys.read().unwrap_or_else(|e| e.into_inner());
    let meta = match keys.get(&key) {
        Some(m) => m.clone(),
        None => return api_error("user not found"),
    };

    let stats = state.user_stats.read().unwrap_or_else(|e| e.into_inner());
    let snapshot = if let Some(st) = stats.get(&key) {
        UserStatsSnapshot {
            access_key: key.clone(),
            name: meta.name.clone(),
            bytes_up: st.bytes_up.load(Ordering::Relaxed),
            bytes_down: st.bytes_down.load(Ordering::Relaxed),
            connections: st.connections.load(Ordering::Relaxed),
            limit_bytes: st.limit_bytes,
            online: st.connections.load(Ordering::Relaxed) > 0,
            last_seen: None,
        }
    } else {
        UserStatsSnapshot {
            access_key: key.clone(),
            name: meta.name.clone(),
            bytes_up: 0,
            bytes_down: 0,
            connections: 0,
            limit_bytes: meta.limit_bytes,
            online: false,
            last_seen: None,
        }
    };

    (StatusCode::OK, ApiResponse::success(snapshot))
}

async fn handle_create_user(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Json(body): Json<CreateUserRequest>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<String>();
    }

    let key = body.access_key.unwrap_or_else(|| {
        use rand::RngCore;
        let mut buf = [0u8; 16];
        rand::thread_rng().fill_bytes(&mut buf);
        buf.iter().map(|b| format!("{:02x}", b)).collect()
    });

    {
        let mut keys = state.access_keys.write().unwrap_or_else(|e| e.into_inner());
        keys.insert(key.clone(), UserMeta { name: body.name.clone(), limit_bytes: body.limit_bytes });
    }

    let mut stats = state.user_stats.write().unwrap_or_else(|e| e.into_inner());
    stats.insert(key.clone(), Arc::new(UserStats::new(body.limit_bytes)));
    drop(stats);

    if let Err(e) = save_config_keys(&state) {
        tracing::error!("Failed to save config after creating user: {}", e);
        return api_error::<String>("failed to save configuration");
    }

    tracing::info!("API: created user key {}", &key[..8.min(key.len())]);
    (StatusCode::OK, ApiResponse::success(key))
}

async fn delete_user(
    State(state): State<ApiState>,
    Path(key): Path<String>,
    headers: axum::http::HeaderMap,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<String>();
    }

    {
        let mut keys = state.access_keys.write().unwrap_or_else(|e| e.into_inner());
        if keys.remove(&key).is_none() {
            return api_error::<String>("User not found");
        }
    }

    {
        let mut stats = state.user_stats.write().unwrap_or_else(|e| e.into_inner());
        stats.remove(&key);
    }

    if let Err(e) = save_config_keys(&state) {
        tracing::error!("Failed to save config after removing user: {}", e);
        return api_error::<String>("failed to save configuration");
    }

    (StatusCode::OK, ApiResponse::success("User removed".to_string()))
}

async fn update_user(
    State(state): State<ApiState>,
    Path(key): Path<String>,
    headers: axum::http::HeaderMap,
    Json(body): Json<UpdateUserRequest>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<String>();
    }

    {
        let mut keys = state.access_keys.write().unwrap_or_else(|e| e.into_inner());
        if let Some(meta) = keys.get_mut(&key) {
            meta.name = body.name.clone();
            meta.limit_bytes = body.limit_bytes;
        } else {
            return api_error::<String>("User not found");
        }
    }

    {
        let mut stats = state.user_stats.write().unwrap_or_else(|e| e.into_inner());
        let entry = stats.entry(key.clone())
            .or_insert_with(|| Arc::new(UserStats::new(body.limit_bytes)));
        
        *entry = Arc::new(UserStats {
            bytes_up: AtomicU64::new(entry.bytes_up.load(Ordering::Relaxed)),
            bytes_down: AtomicU64::new(entry.bytes_down.load(Ordering::Relaxed)),
            connections: AtomicU64::new(entry.connections.load(Ordering::Relaxed)),
            limit_bytes: body.limit_bytes,
            created_at: entry.created_at,
        });
    }

    if let Err(e) = save_config_keys(&state) {
        tracing::error!("Failed to save config after updating user: {}", e);
        return api_error::<String>("failed to save configuration");
    }

    (StatusCode::OK, ApiResponse::success("User updated".to_string()))
}

async fn handle_set_limit(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Path(key): Path<String>,
    Json(body): Json<SetLimitRequest>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<bool>();
    }

    {
        let mut keys = state.access_keys.write().unwrap_or_else(|e| e.into_inner());
        if let Some(meta) = keys.get_mut(&key) {
            meta.limit_bytes = body.limit_bytes;
        } else {
            return api_error("user not found");
        }
    }

    let mut stats = state.user_stats.write().unwrap_or_else(|e| e.into_inner());
    let entry = stats.entry(key.clone())
        .or_insert_with(|| Arc::new(UserStats::new(body.limit_bytes)));

    *entry = Arc::new(UserStats {
        bytes_up: AtomicU64::new(entry.bytes_up.load(Ordering::Relaxed)),
        bytes_down: AtomicU64::new(entry.bytes_down.load(Ordering::Relaxed)),
        connections: AtomicU64::new(entry.connections.load(Ordering::Relaxed)),
        limit_bytes: body.limit_bytes,
        created_at: entry.created_at,
    });
    drop(stats);

    if let Err(e) = save_config_keys(&state) {
        tracing::error!("Failed to save config after setting user limit: {}", e);
        return api_error("failed to save configuration");
    }

    (StatusCode::OK, ApiResponse::success(true))
}

async fn handle_reset_stats(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Path(key): Path<String>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) {
        return api_unauthorized::<bool>();
    }

    let mut stats = state.user_stats.write().unwrap_or_else(|e| e.into_inner());
    if let Some(us) = stats.get(&key) {
        let limit = us.limit_bytes;
        stats.insert(key.clone(), Arc::new(UserStats::new(limit)));
        (StatusCode::OK, ApiResponse::success(true))
    } else {
        api_error("user not found")
    }
}

// ── Subscription endpoint ────────────────────────────────────────────────────

/// Returns a ready-to-use client configuration for the given access key.
/// No Bearer token required -- the access key itself authenticates the request.
/// Compatible with subscription managers (sub-store, NekoBox, custom panels).
///
/// GET /api/subscribe/{key}
/// Response: JSON client config or ostp:// share link (via Accept header)

async fn handle_subscribe(
    State(state): State<ApiState>,
    Path(key): Path<String>,
    headers: axum::http::HeaderMap,
) -> axum::response::Response {
    use axum::response::IntoResponse;

    // Validate that the key exists in a tightly scoped block to drop the guard
    let key_exists = {
        let keys = state.access_keys.read().unwrap_or_else(|e| e.into_inner());
        keys.contains_key(&key)
    };

    if !key_exists {
        return (StatusCode::NOT_FOUND, Json(serde_json::json!({
            "ok": false,
            "error": "invalid access key"
        }))).into_response();
    }

    let accept = headers.get("accept")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("application/json");

    // If client requests plain text, return ostp:// share link
    if accept.contains("text/plain") {
        let dns_enabled = state.dns_server.config.read().await.enabled;
        let rq = if dns_enabled {
            "?type=udp&owndns=true".to_string()
        } else {
            "?type=udp".to_string()
        };
        let link = format!("ostp://{}@{}:{}{}", key, state.server_host, state.server_port, rq);
        return (StatusCode::OK, Json(serde_json::json!({
            "ok": true,
            "data": link
        }))).into_response();
    }

    // Default: return full client config JSON
    let config = serde_json::json!({
        "mode": "client",
        "server": format!("{}:{}", state.server_host, state.server_port),
        "access_key": key,
        "socks5_bind": "127.0.0.1:1088",
        "tun": {
            "enable": false,
            "dns": "1.1.1.1"
        },
        "exclude": {
            "domains": [],
            "ips": [],
            "processes": []
        },
        "turn": {
            "enabled": false
        },
        "mux": {
            "enabled": false,
            "sessions": 1
        },
        "debug": false
    });

    (StatusCode::OK, Json(serde_json::json!({
        "ok": true,
        "data": config
    }))).into_response()
}


#[cfg(test)]
mod tests {
    use super::*;

    fn make_test_state(webpath: &str) -> ApiState {
        ApiState {
            access_keys: Arc::new(RwLock::new(HashMap::new())),
            user_stats: Arc::new(RwLock::new(HashMap::new())),
            start_time: std::time::Instant::now(),
            session_token: Arc::new(RwLock::new(None)),
            api_token: Some("test-token".to_string()),
            webpath: webpath.to_string(),
            username: "admin".to_string(),
            password_hash: "hash".to_string(),
            server_host: "127.0.0.1".to_string(),
            server_port: 50000,
            config_path: None,
            dns_server: crate::dns::DnsServer::new(Default::default()),
            audit_logs: Arc::new(RwLock::new(Vec::new())),
            router: Arc::new(crate::router::Router::new(None, crate::dns::DnsServer::new(Default::default()), false)),
        }
    }

    #[test]
    fn test_router_creation() {
        let state = make_test_state("bNAzr8Ss");
        let _router = create_api_router(state);
    }

    #[test]
    fn test_router_creation_empty_webpath() {
        let state = make_test_state("");
        let _router = create_api_router(state);
    }
}

async fn handle_get_audit(State(state): State<ApiState>) -> impl IntoResponse {
    let logs = state.audit_logs.read().unwrap();
    ApiResponse::success(logs.clone())
}

async fn handle_create_audit(State(state): State<ApiState>, Json(req): Json<CreateAuditLogRequest>) -> impl IntoResponse {
    let mut logs = state.audit_logs.write().unwrap();
    let id = format!("{:x}", rand::random::<u64>());
    let now = chrono::Local::now();
    let entry = AuditLogEntry {
        id,
        time: now.format("%H:%M:%S").to_string(),
        event_en: req.event_en,
        event_ru: req.event_ru,
        success: req.success,
    };
    logs.insert(0, entry);
    if logs.len() > 100 {
        logs.truncate(100);
    }

    ApiResponse::success(true)
}

// ── Bulk keys & Router Rules ─────────────────────────────────────────────────

#[derive(Deserialize)]
pub struct BulkCreateRequest {
    pub count: usize,
    pub limit_bytes: Option<u64>,
}

async fn handle_bulk_create_users(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Json(payload): Json<BulkCreateRequest>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) { return api_unauthorized(); }
    if payload.count > 1000 {
        return api_error("Count too large, max 1000");
    }

    let mut new_keys = Vec::new();
    let mut keys = match state.access_keys.write() {
        Ok(k) => k,
        Err(e) => e.into_inner(),
    };

    for _ in 0..payload.count {
        let key = uuid::Uuid::new_v4().to_string().replace("-", "")[..16].to_string();
        keys.insert(key.clone(), UserMeta {
            name: None,
            limit_bytes: payload.limit_bytes,
        });
        new_keys.push(key);
    }
    // Save keys by dropping lock, then saving to config
    drop(keys);
    let _ = save_config_keys(&state);

    (StatusCode::OK, ApiResponse::success(new_keys))
}

async fn handle_get_rules(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
) -> impl IntoResponse {
    if !check_token(&state, &headers) { return api_unauthorized(); }
    
    let lock = state.router.outbound_cfg.read().unwrap();
    let rules = if let Some(cfg) = lock.as_ref() {
        cfg.rules.clone()
    } else {
        Vec::new()
    };
    
    (StatusCode::OK, ApiResponse::success(rules))
}

async fn handle_put_rules(
    State(state): State<ApiState>,
    headers: axum::http::HeaderMap,
    Json(new_rules): Json<Vec<OutboundRule>>,
) -> impl IntoResponse {
    if !check_token(&state, &headers) { return api_unauthorized(); }
    
    // Update memory
    #[allow(unused_assignments)]
    let mut updated_outbound = None;
    {
        let mut lock = state.router.outbound_cfg.write().unwrap();
        if let Some(cfg) = lock.as_mut() {
            cfg.rules = new_rules.clone();
            updated_outbound = Some(cfg.clone());
        } else {
            return api_error("Outbound routing is not enabled in config");
        }
    }

    if let Some(cfg) = updated_outbound {
        state.dns_server.update_proxy(Some(&cfg)).await;
    }
    
    // Save to config.json
    if let Some(path) = &state.config_path {
        if let Ok(content) = std::fs::read_to_string(path) {
            let mut cfg: serde_json::Value = serde_json::from_str(&content).unwrap_or_default();
            if let Some(obj) = cfg.as_object_mut() {
                if let Some(outbound) = obj.get_mut("outbound") {
                    if let Some(outbound_obj) = outbound.as_object_mut() {
                        if let Ok(rules_json) = serde_json::to_value(&new_rules) {
                            outbound_obj.insert("rules".to_string(), rules_json);
                        }
                    }
                }
            }
            let _ = std::fs::write(path, serde_json::to_string_pretty(&cfg).unwrap_or_default());
        }
    }
    
    (StatusCode::OK, ApiResponse::success(true))
}

async fn handle_clear_audit(State(state): State<ApiState>) -> impl IntoResponse {
    let mut logs = state.audit_logs.write().unwrap();
    logs.clear();
    ApiResponse::success(())
}


