// synops-node — Hent og vis en node med edges.
//
// Henter en node fra PostgreSQL og viser den med alle tilkoblede edges.
// Støtter rekursiv traversering med --depth for å vise nabolag i grafen.
// To output-formater: markdown (lesbart) og JSON (maskinlesbart).
//
// Output: node-data med edges til stdout (markdown eller JSON).
// Logging: structured tracing til stderr.
//
// Miljøvariabler:
//   DATABASE_URL  — PostgreSQL-tilkobling (påkrevd)
//
// Ref: docs/retninger/unix_filosofi.md, docs/primitiver/nodes.md, docs/primitiver/edges.md

use clap::{Parser, ValueEnum};
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::process;
use uuid::Uuid;

/// Hent og vis en node med edges.
#[derive(Parser)]
#[command(name = "synops-node", about = "Hent/vis en node med edges")]
struct Cli {
    /// Node-ID (UUID)
    node_id: Uuid,

    /// Traverseringsdybde for edges (0 = bare noden, 1 = noden + direkte edges, osv.)
    #[arg(long, default_value_t = 1)]
    depth: u32,

    /// Output-format
    #[arg(long, default_value = "md")]
    format: OutputFormat,
}

#[derive(Clone, ValueEnum)]
enum OutputFormat {
    Json,
    Md,
}

// --- Database-rader ---

#[derive(sqlx::FromRow)]
struct NodeRow {
    id: Uuid,
    node_kind: String,
    title: Option<String>,
    content: Option<String>,
    visibility: String,
    metadata: serde_json::Value,
    created_at: chrono::DateTime<chrono::Utc>,
    created_by: Option<Uuid>,
}

#[derive(sqlx::FromRow)]
struct EdgeRow {
    id: Uuid,
    source_id: Uuid,
    target_id: Uuid,
    edge_type: String,
    metadata: serde_json::Value,
    system: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    created_by: Option<Uuid>,
}

// --- Serialiserbare output-strukturer ---

#[derive(Serialize)]
struct NodeOutput {
    id: Uuid,
    node_kind: String,
    title: Option<String>,
    content: Option<String>,
    visibility: String,
    metadata: serde_json::Value,
    created_at: chrono::DateTime<chrono::Utc>,
    created_by: Option<Uuid>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    edges_out: Vec<EdgeOutput>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    edges_in: Vec<EdgeOutput>,
}

#[derive(Serialize)]
struct EdgeOutput {
    id: Uuid,
    source_id: Uuid,
    target_id: Uuid,
    edge_type: String,
    #[serde(skip_serializing_if = "is_empty_object")]
    metadata: serde_json::Value,
    #[serde(skip_serializing_if = "std::ops::Not::not")]
    system: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    #[serde(skip_serializing_if = "Option::is_none")]
    created_by: Option<Uuid>,
    /// Sammendrag av noden i andre enden av edgen
    #[serde(skip_serializing_if = "Option::is_none")]
    peer_title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    peer_kind: Option<String>,
}

fn is_empty_object(v: &serde_json::Value) -> bool {
    v.as_object().is_some_and(|o| o.is_empty())
}

#[derive(Serialize)]
struct FullOutput {
    node: NodeOutput,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    connected_nodes: Vec<NodeOutput>,
}

#[tokio::main]
async fn main() {
    synops_common::logging::init("synops_node");

    let cli = Cli::parse();

    if let Err(e) = run(cli).await {
        eprintln!("Feil: {e}");
        process::exit(1);
    }
}

async fn run(cli: Cli) -> Result<(), String> {
    let db = synops_common::db::connect().await?;

    // Hent hovednoden
    let root_node = fetch_node(&db, cli.node_id).await?
        .ok_or_else(|| format!("Node {} finnes ikke", cli.node_id))?;

    // Depth 0: bare noden, ingen edges
    let (edges_out, edges_in, peer_info) = if cli.depth == 0 {
        (vec![], vec![], HashMap::new())
    } else {
        // Hent edges for hovednoden
        let (eo, ei) = fetch_edges(&db, cli.node_id).await?;

        // Samle peer-noder vi trenger for visning
        let mut peer_ids: HashSet<Uuid> = HashSet::new();
        for e in &eo {
            peer_ids.insert(e.target_id);
        }
        for e in &ei {
            peer_ids.insert(e.source_id);
        }
        peer_ids.remove(&cli.node_id);

        let pi = fetch_node_summaries(&db, &peer_ids).await?;
        (eo, ei, pi)
    };

    // Berik edges med peer-info
    let edges_out = enrich_edges(edges_out, &peer_info, |e| e.target_id);
    let edges_in = enrich_edges(edges_in, &peer_info, |e| e.source_id);

    let mut root_output = node_to_output(root_node, edges_out, edges_in);

    // Depth > 1: hent connected nodes med deres edges
    let mut connected_nodes = Vec::new();
    if cli.depth > 1 {
        let mut visited: HashSet<Uuid> = HashSet::new();
        visited.insert(cli.node_id);

        // Bygg frontier fra edges på hovednoden
        let mut frontier_set: HashSet<Uuid> = HashSet::new();
        for e in &root_output.edges_out {
            frontier_set.insert(e.target_id);
        }
        for e in &root_output.edges_in {
            frontier_set.insert(e.source_id);
        }
        frontier_set.remove(&cli.node_id);
        let mut frontier: Vec<Uuid> = frontier_set.into_iter().collect();

        for _level in 1..cli.depth {
            if frontier.is_empty() {
                break;
            }

            let mut next_frontier = Vec::new();

            for nid in &frontier {
                if visited.contains(nid) {
                    continue;
                }
                visited.insert(*nid);

                if let Some(node) = fetch_node(&db, *nid).await? {
                    let (eo, ei) = fetch_edges(&db, *nid).await?;

                    // Samle nye peers
                    let mut new_peers = HashSet::new();
                    for e in &eo {
                        if !visited.contains(&e.target_id) {
                            new_peers.insert(e.target_id);
                            next_frontier.push(e.target_id);
                        }
                    }
                    for e in &ei {
                        if !visited.contains(&e.source_id) {
                            new_peers.insert(e.source_id);
                            next_frontier.push(e.source_id);
                        }
                    }

                    let extra_peer_info = fetch_node_summaries(&db, &new_peers).await?;
                    // Merge peer info
                    let mut combined = peer_info.clone();
                    combined.extend(extra_peer_info);

                    let eo = enrich_edges(eo, &combined, |e| e.target_id);
                    let ei = enrich_edges(ei, &combined, |e| e.source_id);

                    connected_nodes.push(node_to_output(node, eo, ei));
                }
            }

            frontier = next_frontier;
        }
    }

    tracing::info!(
        node_id = %cli.node_id,
        edges_out = root_output.edges_out.len(),
        edges_in = root_output.edges_in.len(),
        connected = connected_nodes.len(),
        depth = cli.depth,
        "Node hentet"
    );

    match cli.format {
        OutputFormat::Json => {
            let output = if connected_nodes.is_empty() {
                // Enkel output uten wrapper for depth <= 1
                serde_json::to_string_pretty(&root_output)
            } else {
                let full = FullOutput {
                    node: root_output,
                    connected_nodes,
                };
                serde_json::to_string_pretty(&full)
            };
            println!("{}", output.map_err(|e| format!("JSON-serialisering feilet: {e}"))?);
        }
        OutputFormat::Md => {
            print_markdown(&mut root_output, &connected_nodes);
        }
    }

    Ok(())
}

// --- Database-funksjoner ---

async fn fetch_node(
    db: &sqlx::PgPool,
    id: Uuid,
) -> Result<Option<NodeRow>, String> {
    sqlx::query_as::<_, NodeRow>(
        "SELECT id, node_kind::text, title, content, visibility::text, \
         metadata, created_at, created_by \
         FROM nodes WHERE id = $1",
    )
    .bind(id)
    .fetch_optional(db)
    .await
    .map_err(|e| format!("DB-feil (node): {e}"))
}

async fn fetch_edges(
    db: &sqlx::PgPool,
    node_id: Uuid,
) -> Result<(Vec<EdgeRow>, Vec<EdgeRow>), String> {
    let edges_out = sqlx::query_as::<_, EdgeRow>(
        "SELECT id, source_id, target_id, edge_type, metadata, system, created_at, created_by \
         FROM edges WHERE source_id = $1 ORDER BY edge_type, created_at",
    )
    .bind(node_id)
    .fetch_all(db)
    .await
    .map_err(|e| format!("DB-feil (edges ut): {e}"))?;

    let edges_in = sqlx::query_as::<_, EdgeRow>(
        "SELECT id, source_id, target_id, edge_type, metadata, system, created_at, created_by \
         FROM edges WHERE target_id = $1 ORDER BY edge_type, created_at",
    )
    .bind(node_id)
    .fetch_all(db)
    .await
    .map_err(|e| format!("DB-feil (edges inn): {e}"))?;

    Ok((edges_out, edges_in))
}

async fn fetch_node_summaries(
    db: &sqlx::PgPool,
    ids: &HashSet<Uuid>,
) -> Result<HashMap<Uuid, (Option<String>, String)>, String> {
    if ids.is_empty() {
        return Ok(HashMap::new());
    }

    let ids_vec: Vec<Uuid> = ids.iter().copied().collect();

    // sqlx støtter ikke IN-klausul med Vec direkte, bruk ANY
    let rows = sqlx::query_as::<_, (Uuid, Option<String>, String)>(
        "SELECT id, title, node_kind::text FROM nodes WHERE id = ANY($1)",
    )
    .bind(&ids_vec)
    .fetch_all(db)
    .await
    .map_err(|e| format!("DB-feil (peer-noder): {e}"))?;

    Ok(rows.into_iter().map(|(id, title, kind)| (id, (title, kind))).collect())
}

// --- Transformasjoner ---

fn node_to_output(node: NodeRow, edges_out: Vec<EdgeOutput>, edges_in: Vec<EdgeOutput>) -> NodeOutput {
    NodeOutput {
        id: node.id,
        node_kind: node.node_kind,
        title: node.title,
        content: node.content,
        visibility: node.visibility,
        metadata: node.metadata,
        created_at: node.created_at,
        created_by: node.created_by,
        edges_out,
        edges_in,
    }
}

fn enrich_edges(
    edges: Vec<EdgeRow>,
    peers: &HashMap<Uuid, (Option<String>, String)>,
    peer_id_fn: fn(&EdgeRow) -> Uuid,
) -> Vec<EdgeOutput> {
    edges
        .into_iter()
        .map(|e| {
            let pid = peer_id_fn(&e);
            let (peer_title, peer_kind) = peers
                .get(&pid)
                .map(|(t, k)| (t.clone(), Some(k.clone())))
                .unwrap_or((None, None));
            EdgeOutput {
                id: e.id,
                source_id: e.source_id,
                target_id: e.target_id,
                edge_type: e.edge_type,
                metadata: e.metadata,
                system: e.system,
                created_at: e.created_at,
                created_by: e.created_by,
                peer_title,
                peer_kind,
            }
        })
        .collect()
}

// --- Markdown-formattering ---

fn print_markdown(node: &NodeOutput, connected: &[NodeOutput]) {
    let title = node.title.as_deref().unwrap_or("Uten tittel");
    println!("# {title}\n");
    println!("| Felt | Verdi |");
    println!("|------|-------|");
    println!("| **ID** | `{}` |", node.id);
    println!("| **Kind** | `{}` |", node.node_kind);
    println!("| **Synlighet** | {} |", node.visibility);
    println!("| **Opprettet** | {} |", node.created_at.format("%Y-%m-%d %H:%M"));
    if let Some(cb) = node.created_by {
        println!("| **Opprettet av** | `{cb}` |");
    }

    // Metadata (kompakt)
    if let Some(obj) = node.metadata.as_object() {
        if !obj.is_empty() {
            println!("\n## Metadata\n");
            println!("```json\n{}\n```", serde_json::to_string_pretty(&node.metadata).unwrap_or_default());
        }
    }

    // Content (forkortet)
    if let Some(ref content) = node.content {
        println!("\n## Innhold\n");
        if content.len() > 500 {
            println!("{}...\n", &content[..500]);
        } else {
            println!("{content}\n");
        }
    }

    // Edges ut
    if !node.edges_out.is_empty() {
        println!("## Edges ut ({} stk)\n", node.edges_out.len());
        print_edge_table(&node.edges_out, "target");
    }

    // Edges inn
    if !node.edges_in.is_empty() {
        println!("## Edges inn ({} stk)\n", node.edges_in.len());
        print_edge_table(&node.edges_in, "source");
    }

    // Connected nodes (depth > 1)
    if !connected.is_empty() {
        println!("---\n");
        println!("## Tilkoblede noder ({} stk)\n", connected.len());
        for cn in connected {
            let ct = cn.title.as_deref().unwrap_or("Uten tittel");
            println!("### {ct} (`{}`)\n", cn.node_kind);
            println!("ID: `{}`  |  {}  |  {}\n", cn.id, cn.visibility, cn.created_at.format("%Y-%m-%d %H:%M"));

            if !cn.edges_out.is_empty() {
                println!("**Edges ut ({}):**\n", cn.edges_out.len());
                print_edge_table(&cn.edges_out, "target");
            }
            if !cn.edges_in.is_empty() {
                println!("**Edges inn ({}):**\n", cn.edges_in.len());
                print_edge_table(&cn.edges_in, "source");
            }
        }
    }
}

fn print_edge_table(edges: &[EdgeOutput], peer_col: &str) {
    println!("| Type | {} | Tittel | System |", if peer_col == "target" { "Mål" } else { "Kilde" });
    println!("|------|------|--------|--------|");
    for e in edges {
        let peer_id = if peer_col == "target" { e.target_id } else { e.source_id };
        let pt = e.peer_title.as_deref().unwrap_or("-");
        let pk = e.peer_kind.as_deref().unwrap_or("");
        let kind_suffix = if pk.is_empty() { String::new() } else { format!(" ({})", pk) };
        let sys = if e.system { "ja" } else { "" };
        println!("| `{}` | `{}`{} | {} | {} |", e.edge_type, peer_id, kind_suffix, pt, sys);

        // Vis edge-metadata inline hvis den ikke er tom
        if let Some(obj) = e.metadata.as_object() {
            if !obj.is_empty() {
                let meta_str = serde_json::to_string(&e.metadata).unwrap_or_default();
                println!("|  | ↳ metadata: `{meta_str}` |  |  |");
            }
        }
    }
    println!();
}
