//! Legacy single-blob address book — `GET /api/ab` and `POST /api/ab`.
//!
//! Activated when the operator sets `--ab-legacy-mode=on` (which makes
//! `/api/ab/personal` 404 — the documented signal in CONSOLE_API.md §4.2).
//! The wire shape is a JSON-string field `data` whose contents are a second
//! JSON object: `{tags, peers, tag_colors}`. We translate to/from the
//! normalized M2 schema on the personal AB.

use crate::api::error::ApiError;
use crate::api::middleware::AuthedUser;
use crate::api::state::AppState;
use crate::database::AbPeerRow;
use axum::extract::Extension;
use axum::http::StatusCode;
use axum::Json;
use serde_json::{json, Map, Value};
use std::sync::Arc;

pub async fn get(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
) -> Result<Json<Value>, ApiError> {
    let guid = state
        .db
        .ab_get_or_create_personal(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    // Pull all peers and all tags. Page size 1000 is fine — legacy clients
    // expected a single blob anyway.
    let (_total, peers) = state
        .db
        .ab_list_peers(&guid, 0, 10_000)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let tags = state
        .db
        .ab_list_tags(&guid)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let mut tag_colors = Map::new();
    let tag_names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
    for t in &tags {
        tag_colors.insert(t.name.clone(), Value::from(t.color));
    }
    let peer_arr: Vec<Value> = peers
        .iter()
        .map(|p| {
            json!({
                "id":       p.id,
                "alias":    p.alias,
                "tags":     p.tags,
                "username": p.username,
                "hostname": p.hostname,
                "platform": p.platform,
                "hash":     p.hash,
            })
        })
        .collect();
    let inner = json!({
        "tags":       tag_names,
        "peers":      peer_arr,
        "tag_colors": Value::String(serde_json::to_string(&tag_colors).unwrap_or_default()),
    });
    Ok(Json(json!({ "data": inner.to_string() })))
}

#[derive(serde::Deserialize)]
pub struct LegacyPostBody {
    pub data: String,
}

pub async fn put(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Json(body): Json<LegacyPostBody>,
) -> Result<StatusCode, ApiError> {
    let guid = state
        .db
        .ab_get_or_create_personal(user.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let inner: Value = serde_json::from_str(&body.data)
        .map_err(|e| ApiError::BadRequest(format!("data is not valid json: {}", e)))?;
    // Tag colors are stored as a JSON-encoded string field (Flutter wraps
    // the map in another JSON layer). Tolerate either an inline map or the
    // doubly-encoded form.
    let tag_colors_map: Map<String, Value> = match inner.get("tag_colors") {
        Some(Value::String(s)) => serde_json::from_str(s).unwrap_or_default(),
        Some(Value::Object(m)) => m.clone(),
        _ => Map::new(),
    };
    let tag_names: Vec<String> = inner
        .get("tags")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();
    let tags: Vec<(String, i64)> = tag_names
        .iter()
        .map(|n| {
            let color = tag_colors_map
                .get(n)
                .and_then(|v| v.as_i64())
                .unwrap_or(0);
            (n.clone(), color)
        })
        .collect();

    let peer_arr = inner
        .get("peers")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();
    let mut peers: Vec<AbPeerRow> = Vec::with_capacity(peer_arr.len());
    for p in peer_arr {
        let id = p
            .get("id")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string();
        if id.is_empty() {
            continue;
        }
        let tags = p
            .get("tags")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();
        peers.push(AbPeerRow {
            id,
            alias: get_str(&p, "alias"),
            note: String::new(),
            password: String::new(),
            hash: get_str(&p, "hash"),
            username: get_str(&p, "username"),
            hostname: get_str(&p, "hostname"),
            platform: get_str(&p, "platform"),
            tags,
        });
    }
    state
        .db
        .ab_legacy_replace(&guid, &tags, &peers)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(StatusCode::OK)
}

fn get_str(v: &Value, k: &str) -> String {
    v.get(k)
        .and_then(|x| x.as_str())
        .unwrap_or_default()
        .to_string()
}
