// synops-suggest-edges — AI-foreslåtte edges for en node.
//
// Input: --node-id <uuid>. Henter nodens innhold fra PG, sender til LiteLLM
// for analyse, returnerer foreslåtte topics og mentions som JSON.
// Med --write: oppretter topic-noder og mentions-edges i PG.
//
// Miljøvariabler:
//   DATABASE_URL       — PostgreSQL-tilkobling (påkrevd)
//   AI_GATEWAY_URL     — LiteLLM gateway (default: http://localhost:4000)
//   LITELLM_MASTER_KEY — API-nøkkel for LiteLLM
//   AI_EDGES_MODEL     — Modellalias (default: synops/low)
//
// Erstatter: maskinrommet/src/ai_edges.rs
// Ref: docs/retninger/unix_filosofi.md, docs/infra/ai_gateway.md,
//      docs/concepts/kunnskapsgrafen.md

use clap::Parser;
use serde::{Deserialize, Serialize};
use std::process;
use uuid::Uuid;

/// AI-foreslåtte edges (topics og mentions) for en node via LiteLLM.
#[derive(Parser)]
#[command(name = "synops-suggest-edges", about = "Foreslå AI-genererte edges for en node")]
struct Cli {
    /// Node-ID som skal analyseres
    #[arg(long)]
    node_id: Uuid,

    /// Bruker-ID som utløste analysen
    #[arg(long)]
    requested_by: Option<Uuid>,

    /// Skriv topic-noder og mentions-edges til database (uten: kun forslag + stdout)
    #[arg(long)]
    write: bool,
}

// --- Database-rader ---

#[derive(sqlx::FromRow)]
struct SourceNode {
    title: Option<String>,
    content: Option<String>,
    created_by: Option<Uuid>,
}

#[derive(sqlx::FromRow)]
struct TopicRow {
    id: Uuid,
    title: String,
}

// --- LLM request/response (OpenAI-kompatibel) ---

#[derive(Serialize)]
struct ChatRequest {
    model: String,
    messages: Vec<ChatMessage>,
    temperature: f32,
    response_format: ResponseFormat,
}

#[derive(Serialize)]
struct ResponseFormat {
    r#type: String,
}

#[derive(Serialize)]
struct ChatMessage {
    role: String,
    content: String,
}

#[derive(Deserialize)]
struct ChatResponse {
    choices: Vec<Choice>,
    #[serde(default)]
    usage: Option<UsageInfo>,
    #[serde(default)]
    model: Option<String>,
}

#[derive(Deserialize, Clone)]
struct UsageInfo {
    #[serde(default)]
    prompt_tokens: i64,
    #[serde(default)]
    completion_tokens: i64,
}

#[derive(Deserialize)]
struct Choice {
    message: MessageContent,
}

#[derive(Deserialize)]
struct MessageContent {
    content: Option<String>,
}

// --- LLM-analysens output ---

#[derive(Deserialize, Debug)]
struct AiSuggestion {
    #[serde(default)]
    topics: Vec<String>,
    #[serde(default)]
    mentions: Vec<MentionSuggestion>,
}

#[derive(Deserialize, Debug)]
struct MentionSuggestion {
    name: String,
    #[serde(default = "default_entity_type")]
    entity_type: String,
}

fn default_entity_type() -> String {
    "person".to_string()
}

const SYSTEM_PROMPT: &str = r#"Du er en innholdsanalysator for en norsk redaksjonsplattform. Analyser teksten og ekstraher:

1. **topics**: Emner/temaer teksten handler om. Bruk korte, presise norske termer (f.eks. "skolepolitikk", "klimaendringer", "statsbudsjettet"). Maks 5 topics.

2. **mentions**: Navngitte entiteter (personer, organisasjoner, steder) som er eksplisitt nevnt. Inkluder entity_type ("person", "organisasjon", "sted", "konsept").

Returner KUN et JSON-objekt med denne strukturen:
{
  "topics": ["emne1", "emne2"],
  "mentions": [{"name": "Navn", "entity_type": "person"}]
}

Regler:
- Returner tom liste hvis teksten ikke har meningsfullt innhold (hilsener, korte svar, etc.)
- Bruk eksisterende topics fra listen nedenfor der det passer, i stedet for å lage nye varianter
- Ikke inkluder generiske termer som "samtale" eller "diskusjon"
- Navngi entiteter med full, autoritativ form (f.eks. "Jonas Gahr Støre", ikke "Støre")"#;

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

    let cli = Cli::parse();

    if cli.write && cli.requested_by.is_none() {
        eprintln!("Feil: --requested-by er påkrevd sammen med --write");
        process::exit(1);
    }

    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?;

    let node_id = cli.node_id;

    // 1. Hent kildenode
    let source = sqlx::query_as::<_, SourceNode>(
        "SELECT title, content, created_by FROM nodes WHERE id = $1",
    )
    .bind(node_id)
    .fetch_optional(&db)
    .await
    .map_err(|e| format!("PG-feil ved henting av node: {e}"))?
    .ok_or_else(|| format!("Node {node_id} finnes ikke"))?;

    let title = source.title.unwrap_or_default();
    let content = source.content.unwrap_or_default();

    // Ikke analyser tomme noder eller veldig korte meldinger
    let text = format!("{title}\n{content}").trim().to_string();
    if text.len() < 20 {
        tracing::info!(node_id = %node_id, len = text.len(), "For kort innhold, hopper over");
        let result = serde_json::json!({
            "status": "skipped",
            "reason": "content_too_short",
            "node_id": node_id.to_string()
        });
        println!("{}", serde_json::to_string_pretty(&result).unwrap());
        return Ok(());
    }

    // 2. Hent eksisterende topic-noder for kontekst
    let existing_topics = sqlx::query_as::<_, TopicRow>(
        "SELECT id, title FROM nodes WHERE node_kind = 'topic' ORDER BY created_at DESC LIMIT 100",
    )
    .fetch_all(&db)
    .await
    .map_err(|e| format!("PG-feil ved henting av topics: {e}"))?;

    let topic_list: Vec<&str> = existing_topics.iter().map(|t| t.title.as_str()).collect();

    // 3. Bygg prompt og kall LiteLLM
    let user_content = if topic_list.is_empty() {
        format!("Analyser følgende tekst:\n\n{text}")
    } else {
        format!(
            "Eksisterende topics: {}\n\nAnalyser følgende tekst:\n\n{text}",
            topic_list.join(", ")
        )
    };

    tracing::info!(node_id = %node_id, "Sender til LLM for edge-analyse");
    let (suggestion, llm_usage, llm_model) = call_llm(&user_content).await?;

    tracing::info!(
        node_id = %node_id,
        topics = ?suggestion.topics,
        mentions = suggestion.mentions.len(),
        "LLM-forslag mottatt"
    );

    // 4. Bygg forslag-liste med confidence og target-info
    let mut suggestions = Vec::new();

    for topic_name in &suggestion.topics {
        let topic_name = topic_name.trim();
        if topic_name.is_empty() {
            continue;
        }

        let existing = existing_topics
            .iter()
            .find(|t| t.title.to_lowercase() == topic_name.to_lowercase());

        suggestions.push(serde_json::json!({
            "target": topic_name,
            "target_id": existing.map(|t| t.id.to_string()),
            "edge_type": "mentions",
            "kind": "topic",
            "confidence": 0.8,
            "exists": existing.is_some()
        }));
    }

    for mention in &suggestion.mentions {
        let name = mention.name.trim();
        if name.is_empty() {
            continue;
        }

        // Sjekk om entiteten allerede finnes
        let existing_entity = sqlx::query_scalar::<_, Uuid>(
            "SELECT id FROM nodes WHERE node_kind = 'topic' AND LOWER(title) = LOWER($1) LIMIT 1",
        )
        .bind(name)
        .fetch_optional(&db)
        .await
        .map_err(|e| format!("PG-feil ved entitet-søk: {e}"))?;

        suggestions.push(serde_json::json!({
            "target": name,
            "target_id": existing_entity.map(|id| id.to_string()),
            "edge_type": "mentions",
            "kind": mention.entity_type,
            "confidence": 0.9,
            "exists": existing_entity.is_some()
        }));
    }

    let tokens_in = llm_usage.as_ref().map(|u| u.prompt_tokens).unwrap_or(0);
    let tokens_out = llm_usage.as_ref().map(|u| u.completion_tokens).unwrap_or(0);
    let model_id = llm_model.unwrap_or_else(|| "unknown".to_string());

    // 5. Skriv til database hvis --write
    let result = if cli.write {
        let requested_by = cli.requested_by.unwrap(); // Allerede validert
        let created_by = source.created_by.unwrap_or(node_id);
        let (topics_created, edges_created) =
            write_to_db(&db, node_id, &suggestion, &existing_topics, created_by).await?;

        // Logg AI-ressursforbruk
        log_resource_usage(&db, node_id, source.created_by, &model_id, tokens_in, tokens_out, requested_by)
            .await;

        serde_json::json!({
            "status": "completed",
            "node_id": node_id.to_string(),
            "suggestions": suggestions,
            "topics_created": topics_created,
            "edges_created": edges_created,
            "model": model_id,
            "tokens_in": tokens_in,
            "tokens_out": tokens_out,
        })
    } else {
        serde_json::json!({
            "status": "completed",
            "node_id": node_id.to_string(),
            "suggestions": suggestions,
            "model": model_id,
            "tokens_in": tokens_in,
            "tokens_out": tokens_out,
        })
    };

    // Output JSON til stdout
    println!(
        "{}",
        serde_json::to_string_pretty(&result)
            .map_err(|e| format!("JSON-serialisering feilet: {e}"))?
    );

    Ok(())
}

/// Kall LiteLLM for innholdsanalyse. Returnerer (forslag, usage, model).
async fn call_llm(user_content: &str) -> Result<(AiSuggestion, Option<UsageInfo>, Option<String>), String> {
    let gateway_url =
        std::env::var("AI_GATEWAY_URL").unwrap_or_else(|_| "http://localhost:4000".to_string());
    let api_key = std::env::var("LITELLM_MASTER_KEY").unwrap_or_default();

    let model =
        std::env::var("AI_EDGES_MODEL").unwrap_or_else(|_| "synops/low".to_string());

    let request = ChatRequest {
        model,
        messages: vec![
            ChatMessage {
                role: "system".to_string(),
                content: SYSTEM_PROMPT.to_string(),
            },
            ChatMessage {
                role: "user".to_string(),
                content: user_content.to_string(),
            },
        ],
        temperature: 0.2,
        response_format: ResponseFormat {
            r#type: "json_object".to_string(),
        },
    };

    let client = reqwest::Client::new();
    let url = format!("{gateway_url}/v1/chat/completions");

    let resp = client
        .post(&url)
        .header("Authorization", format!("Bearer {api_key}"))
        .header("Content-Type", "application/json")
        .json(&request)
        .timeout(std::time::Duration::from_secs(30))
        .send()
        .await
        .map_err(|e| format!("LiteLLM-kall feilet: {e}"))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("LiteLLM returnerte {status}: {body}"));
    }

    let chat_resp: ChatResponse = resp
        .json()
        .await
        .map_err(|e| format!("Kunne ikke parse LiteLLM-respons: {e}"))?;

    let content = chat_resp
        .choices
        .first()
        .and_then(|c| c.message.content.as_deref())
        .ok_or("LiteLLM returnerte ingen content")?;

    let suggestion: AiSuggestion = serde_json::from_str(content)
        .map_err(|e| format!("Kunne ikke parse LLM JSON: {e}. Rå output: {content}"))?;

    Ok((suggestion, chat_resp.usage, chat_resp.model))
}

/// Opprett topic-noder og mentions-edges i PG.
/// Returnerer (topics_created, edges_created).
async fn write_to_db(
    db: &sqlx::PgPool,
    node_id: Uuid,
    suggestion: &AiSuggestion,
    existing_topics: &[TopicRow],
    created_by: Uuid,
) -> Result<(u32, u32), String> {
    let mut topics_created = 0u32;
    let mut edges_created = 0u32;

    // Prosesser topics
    for topic_name in &suggestion.topics {
        let topic_name = topic_name.trim();
        if topic_name.is_empty() {
            continue;
        }

        let existing = existing_topics
            .iter()
            .find(|t| t.title.to_lowercase() == topic_name.to_lowercase());

        let topic_id = if let Some(t) = existing {
            t.id
        } else {
            let new_id = Uuid::now_v7();
            create_topic_node(db, new_id, topic_name, created_by).await?;
            topics_created += 1;
            new_id
        };

        if create_mentions_edge(db, node_id, topic_id, created_by).await? {
            edges_created += 1;
        }
    }

    // Prosesser mentions (entiteter)
    for mention in &suggestion.mentions {
        let name = mention.name.trim();
        if name.is_empty() {
            continue;
        }

        let existing_entity = sqlx::query_scalar::<_, Uuid>(
            "SELECT id FROM nodes WHERE node_kind = 'topic' AND LOWER(title) = LOWER($1) LIMIT 1",
        )
        .bind(name)
        .fetch_optional(db)
        .await
        .map_err(|e| format!("PG-feil ved entitet-søk: {e}"))?;

        let entity_id = if let Some(id) = existing_entity {
            id
        } else {
            let new_id = Uuid::now_v7();
            create_entity_node(db, new_id, name, &mention.entity_type, created_by).await?;
            topics_created += 1;
            new_id
        };

        if create_mentions_edge(db, node_id, entity_id, created_by).await? {
            edges_created += 1;
        }
    }

    tracing::info!(
        node_id = %node_id,
        topics_created = topics_created,
        edges_created = edges_created,
        "AI edge-forslag skrevet til database"
    );

    Ok((topics_created, edges_created))
}

/// Opprett en topic-node i PG.
async fn create_topic_node(
    db: &sqlx::PgPool,
    id: Uuid,
    title: &str,
    created_by: Uuid,
) -> Result<(), String> {
    let metadata = serde_json::json!({"ai_generated": true});

    sqlx::query(
        r#"
        INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
        VALUES ($1, 'topic', $2, '', 'discoverable', $3, $4)
        ON CONFLICT (id) DO NOTHING
        "#,
    )
    .bind(id)
    .bind(title)
    .bind(&metadata)
    .bind(created_by)
    .execute(db)
    .await
    .map_err(|e| format!("PG insert topic feilet: {e}"))?;

    tracing::info!(topic_id = %id, title = %title, "Ny topic-node opprettet (AI)");
    Ok(())
}

/// Opprett en entitet-node (person, org, sted) i PG.
async fn create_entity_node(
    db: &sqlx::PgPool,
    id: Uuid,
    name: &str,
    entity_type: &str,
    created_by: Uuid,
) -> Result<(), String> {
    let metadata = serde_json::json!({
        "ai_generated": true,
        "entity_type": entity_type
    });

    sqlx::query(
        r#"
        INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
        VALUES ($1, 'topic', $2, '', 'discoverable', $3, $4)
        ON CONFLICT (id) DO NOTHING
        "#,
    )
    .bind(id)
    .bind(name)
    .bind(&metadata)
    .bind(created_by)
    .execute(db)
    .await
    .map_err(|e| format!("PG insert entity feilet: {e}"))?;

    tracing::info!(entity_id = %id, name = %name, entity_type = %entity_type, "Ny entitet-node opprettet (AI)");
    Ok(())
}

/// Opprett en mentions-edge. Returnerer true hvis ny edge ble opprettet.
async fn create_mentions_edge(
    db: &sqlx::PgPool,
    source_id: Uuid,
    target_id: Uuid,
    created_by: Uuid,
) -> Result<bool, String> {
    let exists = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS(SELECT 1 FROM edges WHERE source_id = $1 AND target_id = $2 AND edge_type = 'mentions')",
    )
    .bind(source_id)
    .bind(target_id)
    .fetch_one(db)
    .await
    .map_err(|e| format!("PG-feil ved edge-sjekk: {e}"))?;

    if exists {
        return Ok(false);
    }

    let edge_id = Uuid::now_v7();
    let metadata = serde_json::json!({"origin": "ai"});

    sqlx::query(
        r#"
        INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
        VALUES ($1, $2, $3, 'mentions', $4, false, $5)
        ON CONFLICT (source_id, target_id, edge_type) DO NOTHING
        "#,
    )
    .bind(edge_id)
    .bind(source_id)
    .bind(target_id)
    .bind(&metadata)
    .bind(created_by)
    .execute(db)
    .await
    .map_err(|e| format!("PG insert mentions-edge feilet: {e}"))?;

    tracing::info!(
        edge_id = %edge_id,
        source = %source_id,
        target = %target_id,
        "Mentions-edge opprettet (AI)"
    );

    Ok(true)
}

/// Logg AI-ressursforbruk til resource_usage_log.
async fn log_resource_usage(
    db: &sqlx::PgPool,
    node_id: Uuid,
    _created_by: Option<Uuid>,
    model_id: &str,
    tokens_in: i64,
    tokens_out: i64,
    requested_by: Uuid,
) {
    // Finn eventuell collection
    let collection_id: Option<Uuid> = sqlx::query_scalar(
        "SELECT e.target_id FROM edges e
         JOIN nodes n ON n.id = e.target_id
         WHERE e.source_id = $1 AND e.edge_type = 'belongs_to' AND n.node_kind = 'collection'
         LIMIT 1",
    )
    .bind(node_id)
    .fetch_optional(db)
    .await
    .ok()
    .flatten();

    if let Err(e) = sqlx::query(
        "INSERT INTO resource_usage_log (target_node_id, triggered_by, collection_id, resource_type, detail)
         VALUES ($1, $2, $3, $4, $5)",
    )
    .bind(node_id)
    .bind(Some(requested_by))
    .bind(collection_id)
    .bind("ai")
    .bind(serde_json::json!({
        "model_level": "fast",
        "model_id": model_id,
        "tokens_in": tokens_in,
        "tokens_out": tokens_out,
        "job_type": "suggest_edges"
    }))
    .execute(db)
    .await
    {
        tracing::warn!(error = %e, "Kunne ikke logge AI-ressursforbruk");
    }
}
