// synops-ai — AI-assistert oppretting av orkestreringsscript.
//
// Leser cli_tool-noder fra PG, bygger en systemprompt med tilgjengelige
// verktøy og script-grammatikk, og bruker LLM til å generere et
// orkestreringsscript fra en fritekst-beskrivelse.
//
// Tre moduser:
//   1. Generer script:      --description "..." [--trigger-event ...]
//   2. System prompt:        --generate-system-prompt (skriver prompt til stdout)
//   3. Eventually-modus:     --description "..." --eventually (lagrer som work_item)
//
// 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_SCRIPT_MODEL    — Modellalias (default: sidelinja/smart)
//
// Ref: docs/concepts/orkestrering.md § "Nivå 2: AI-assistert oppretting"

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

/// AI-assistert oppretting av orkestreringsscript.
#[derive(Parser)]
#[command(
    name = "synops-ai",
    about = "Generer orkestreringsscript fra fritekst-beskrivelse via LLM"
)]
struct Cli {
    /// Fritekst-beskrivelse av ønsket orkestrering
    #[arg(long)]
    description: Option<String>,

    /// Trigger-event for scriptet (f.eks. "communication.ended")
    #[arg(long)]
    trigger_event: Option<String>,

    /// Trigger-betingelser som JSON (f.eks. '{"has_trait":"podcast"}')
    #[arg(long)]
    trigger_conditions: Option<String>,

    /// Kun skriv ut auto-generert systemprompt (ingen LLM-kall)
    #[arg(long)]
    generate_system_prompt: bool,

    /// Eventually-modus: lagre forespørselen som work_item i stedet for synkront LLM-kall
    #[arg(long)]
    eventually: bool,

    /// Bruker-ID som utløste forespørselen (påkrevd for --eventually)
    #[arg(long)]
    requested_by: Option<Uuid>,

    /// Samlings-ID å knytte work_item til
    #[arg(long)]
    collection_id: Option<Uuid>,
}

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

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

#[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>,
}

// --- cli_tool metadata fra PG ---

#[derive(sqlx::FromRow)]
struct CliToolRow {
    #[allow(dead_code)]
    title: Option<String>,
    metadata: Option<serde_json::Value>,
}

/// Informasjon om et CLI-verktøy hentet fra PG.
struct ToolInfo {
    binary: String,
    description: String,
    aliases: Vec<String>,
    args_hints: Vec<(String, String)>,
}

fn extract_tool_info(row: &CliToolRow) -> Option<ToolInfo> {
    let meta = row.metadata.as_ref()?;
    let binary = meta.get("binary")?.as_str()?.to_string();
    let description = meta
        .get("description")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let aliases: Vec<String> = meta
        .get("aliases")
        .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 args_hints: Vec<(String, String)> = meta
        .get("args_hints")
        .and_then(|v| v.as_object())
        .map(|obj| {
            obj.iter()
                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                .collect()
        })
        .unwrap_or_default();
    Some(ToolInfo {
        binary,
        description,
        aliases,
        args_hints,
    })
}

/// Bygg systemprompt fra cli_tool-noder og script-grammatikk.
fn build_system_prompt(tools: &[ToolInfo]) -> String {
    let mut prompt = String::new();

    prompt.push_str(
        "Du er en orkestreringsplanlegger for Synops, en norsk redaksjonsplattform.\n\
         \n\
         Du genererer orkestreringsscript i Synops sitt menneskelige scriptspråk.\n\
         Scriptet kompileres automatisk til CLI-kall av vaktmesteren.\n\
         \n\
         VIKTIG: Skriv scriptet i det menneskelige laget — bruk norske verb og \n\
         argumenter i parenteser. IKKE skriv CLI-flagg eller teknisk syntax.\n\
         \n",
    );

    // Tilgjengelige verktøy
    prompt.push_str("TILGJENGELIGE VERKTØY:\n\n");
    for tool in tools {
        prompt.push_str(&format!("- {}: {}\n", tool.binary, tool.description));
        if !tool.aliases.is_empty() {
            prompt.push_str(&format!("  Verb: {}\n", tool.aliases.join(", ")));
        }
        if !tool.args_hints.is_empty() {
            prompt.push_str("  Argumenter (bruk i parenteser):\n");
            for (human, _technical) in &tool.args_hints {
                prompt.push_str(&format!("    - \"{human}\"\n"));
            }
        }
        prompt.push('\n');
    }

    // Script-grammatikk
    prompt.push_str(
        "SCRIPT-GRAMMATIKK (menneskelig lag):\n\
         \n\
         ```\n\
         NÅR <event i naturlig språk>\n\
         HVIS <betingelse i naturlig språk>\n\
         \n\
         <N>. <verb> <objekt> [(<argument1>, <argument2>)]\n\
         <N+1>. <verb> <objekt>\n\
         \n\
         ved feil: opprett oppgave \"<tittel>\" (<tag>)\n\
         ```\n\
         \n\
         Regler:\n\
         - NÅR og HVIS er valgfrie (trigger settes separat i metadata)\n\
         - Verb må matche ett av verktøyenes alias-verb\n\
         - Argumenter i parentes må matche verktøyets argumenter nøyaktig\n\
         - Hvert steg nummereres sekvensielt (1, 2, 3...)\n\
         - Steg-spesifikk feilhåndtering med innrykk:\n\
         ```\n\
         1. transkriber lydfilen (stor modell)\n\
         \x20  ved feil: transkriber lydfilen (medium modell)\n\
         ```\n\
         - Global feilhåndtering (uten innrykk) på siste linje\n\
         - \"opprett oppgave\" er alltid tilgjengelig for feilhåndtering\n\
         \n",
    );

    // Trigger-events
    prompt.push_str(
        "KJENTE TRIGGER-EVENTS:\n\
         - node.created — Ny node opprettet\n\
         - edge.created — Ny edge opprettet\n\
         - communication.ended — Samtale/innspilling avsluttet\n\
         - node.published — Node publisert\n\
         - scheduled.due — Planlagt tidspunkt nådd\n\
         - manual — Bruker trykker \"Kjør\"\n\
         \n",
    );

    // Eksempel
    prompt.push_str(
        "EKSEMPEL:\n\
         Beskrivelse: \"Når en innspilling er ferdig, transkriber og oppsummer\"\n\
         \n\
         Resultat:\n\
         ```\n\
         1. transkriber lydfilen (stor modell)\n\
         \x20  ved feil: transkriber lydfilen (medium modell)\n\
         2. oppsummer samtalen\n\
         \n\
         ved feil: opprett oppgave \"Pipeline feilet\" (bug)\n\
         ```\n\
         \n\
         EKSEMPEL 2:\n\
         Beskrivelse: \"Publiser artikkelen og oppdater RSS\"\n\
         \n\
         Resultat:\n\
         ```\n\
         1. render artikkelen\n\
         2. oppdater rss-feed\n\
         \n\
         ved feil: opprett oppgave \"Publisering feilet\" (bug)\n\
         ```\n\
         \n",
    );

    // Instruksjoner
    prompt.push_str(
        "INSTRUKSJONER:\n\
         1. Les brukerens beskrivelse nøye.\n\
         2. Velg de riktige verktøyene og argumentene.\n\
         3. Skriv et gyldig script i det menneskelige laget.\n\
         4. Bruk feilhåndtering der det er naturlig (VED_FEIL eller fallback-modell).\n\
         5. Svar KUN med scriptet — ingen forklaringer, ingen markdown-blokker, \n\
            ingen kommentarer. Bare rene script-linjer.\n\
         6. Hvis beskrivelsen refererer til funksjonalitet som IKKE finnes i \n\
            tilgjengelige verktøy, bruk verbet/objektet likevel — kompilatoren \n\
            vil rapportere feilen, og brukeren kan opprette en forespørsel.\n",
    );

    prompt
}

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

    let cli = Cli::parse();

    // Validering
    if !cli.generate_system_prompt && cli.description.is_none() {
        eprintln!("Feil: --description eller --generate-system-prompt er påkrevd");
        process::exit(1);
    }

    if cli.eventually && cli.requested_by.is_none() {
        eprintln!("Feil: --requested-by er påkrevd sammen med --eventually");
        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?;

    // Hent alle cli_tool-noder
    let rows = sqlx::query_as::<_, CliToolRow>(
        "SELECT title, metadata FROM nodes WHERE node_kind = 'cli_tool' ORDER BY title",
    )
    .fetch_all(&db)
    .await
    .map_err(|e| format!("PG-feil ved henting av cli_tool-noder: {e}"))?;

    let tools: Vec<ToolInfo> = rows.iter().filter_map(extract_tool_info).collect();
    tracing::info!(tool_count = tools.len(), "Hentet cli_tool-noder");

    let system_prompt = build_system_prompt(&tools);

    // Modus 1: Bare skriv ut systemprompt
    if cli.generate_system_prompt {
        println!("{system_prompt}");
        return Ok(());
    }

    let description = cli.description.as_deref().unwrap();

    // Modus 3: Eventually — lagre som work_item
    if cli.eventually {
        return save_work_item(
            &db,
            description,
            cli.trigger_event.as_deref(),
            cli.trigger_conditions.as_deref(),
            cli.requested_by.unwrap(),
            cli.collection_id,
        )
        .await;
    }

    // Modus 2: Synkron AI-generering
    let user_content = build_user_prompt(
        description,
        cli.trigger_event.as_deref(),
        cli.trigger_conditions.as_deref(),
    );

    tracing::info!(description_len = description.len(), "Sender til LLM for script-generering");

    let (generated_script, llm_usage, llm_model) =
        call_llm(&system_prompt, &user_content).await?;

    tracing::info!(
        script_len = generated_script.len(),
        "Script generert fra LLM"
    );

    // Enkel sjekk om scriptet har nummererte steg
    // Full validering skjer i vaktmesteren via script_compiler
    let has_numbered_steps = generated_script
        .lines()
        .any(|line| {
            let trimmed = line.trim();
            trimmed.len() > 2
                && trimmed.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
                && trimmed.contains('.')
        });

    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());

    // Logg AI-forbruk
    if let Err(e) = sqlx::query(
        "INSERT INTO resource_usage_log (target_node_id, triggered_by, resource_type, detail)
         VALUES ($1, $2, $3, $4)",
    )
    .bind(cli.collection_id) // target_node_id (kan være null)
    .bind(cli.requested_by)
    .bind("ai")
    .bind(serde_json::json!({
        "model_id": model_id,
        "tokens_in": tokens_in,
        "tokens_out": tokens_out,
        "job_type": "orchestration_script_generation"
    }))
    .execute(&db)
    .await
    {
        tracing::warn!(error = %e, "Kunne ikke logge AI-ressursforbruk");
    }

    // Bygg resultat
    let result = serde_json::json!({
        "status": "completed",
        "script": generated_script.trim(),
        "has_steps": has_numbered_steps,
        "model": model_id,
        "tokens_in": tokens_in,
        "tokens_out": tokens_out,
        "tool_count": tools.len(),
    });

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

    Ok(())
}

/// Bygg bruker-prompt med beskrivelse og trigger-info.
fn build_user_prompt(
    description: &str,
    trigger_event: Option<&str>,
    trigger_conditions: Option<&str>,
) -> String {
    let mut prompt = format!("Lag et orkestreringsscript for: {description}");
    if let Some(event) = trigger_event {
        prompt.push_str(&format!("\n\nTrigger-event: {event}"));
    }
    if let Some(conditions) = trigger_conditions {
        prompt.push_str(&format!("\nBetingelser: {conditions}"));
    }
    prompt
}

/// Kall LiteLLM for script-generering. Returnerer (script, usage, model).
async fn call_llm(
    system_prompt: &str,
    user_content: &str,
) -> Result<(String, 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_SCRIPT_MODEL").unwrap_or_else(|_| "sidelinja/rutine".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.3,
    };

    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(60))
        .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")?;

    // Strip eventuelle markdown-blokker som LLM kan ha lagt til
    let cleaned = strip_markdown_fences(content);

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

/// Fjern markdown code fences hvis LLM wrapper scriptet i ```...```
fn strip_markdown_fences(text: &str) -> String {
    let trimmed = text.trim();
    if trimmed.starts_with("```") {
        let lines: Vec<&str> = trimmed.lines().collect();
        if lines.len() >= 2 {
            let start = 1; // Hopp over åpnings-fence
            let end = if lines.last().map(|l| l.trim()) == Some("```") {
                lines.len() - 1
            } else {
                lines.len()
            };
            return lines[start..end].join("\n");
        }
    }
    trimmed.to_string()
}

/// Lagre forespørselen som work_item for Claude Code (eventually-modus).
async fn save_work_item(
    db: &sqlx::PgPool,
    description: &str,
    trigger_event: Option<&str>,
    trigger_conditions: Option<&str>,
    requested_by: Uuid,
    collection_id: Option<Uuid>,
) -> Result<(), String> {
    let work_item_id = Uuid::now_v7();
    let title = format!("AI-script: {}", truncate(description, 80));

    let metadata = serde_json::json!({
        "work_item_type": "script_request",
        "description": description,
        "trigger_event": trigger_event,
        "trigger_conditions": trigger_conditions,
    });

    // Opprett work_item-node
    sqlx::query(
        r#"
        INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
        VALUES ($1, 'content', $2, $3, 'hidden'::visibility, $4, $5)
        "#,
    )
    .bind(work_item_id)
    .bind(&title)
    .bind(description)
    .bind(&metadata)
    .bind(requested_by)
    .execute(db)
    .await
    .map_err(|e| format!("PG insert work_item feilet: {e}"))?;

    // Legg til tagged-edge med "script_request"
    let tag_edge_id = Uuid::now_v7();
    sqlx::query(
        r#"
        INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
        VALUES ($1, $2, $2, 'tagged', '{"tag": "script_request"}'::jsonb, true, $3)
        "#,
    )
    .bind(tag_edge_id)
    .bind(work_item_id)
    .bind(requested_by)
    .execute(db)
    .await
    .map_err(|e| format!("PG insert tagged-edge feilet: {e}"))?;

    // Knytt til samling hvis oppgitt
    if let Some(coll_id) = collection_id {
        let belongs_edge_id = Uuid::now_v7();
        sqlx::query(
            r#"
            INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
            VALUES ($1, $2, $3, 'belongs_to', '{}'::jsonb, false, $4)
            "#,
        )
        .bind(belongs_edge_id)
        .bind(work_item_id)
        .bind(coll_id)
        .bind(requested_by)
        .execute(db)
        .await
        .map_err(|e| format!("PG insert belongs_to-edge feilet: {e}"))?;
    }

    tracing::info!(
        work_item_id = %work_item_id,
        "Work item opprettet for script-forespørsel"
    );

    let result = serde_json::json!({
        "status": "deferred",
        "work_item_id": work_item_id.to_string(),
        "title": title,
        "message": "Forespørselen er lagret som work_item. Claude Code vil generere scriptet i neste sesjon.",
    });

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

    Ok(())
}

/// Trunkér en streng til maks lengde.
fn truncate(s: &str, max_len: usize) -> &str {
    if s.len() <= max_len {
        s
    } else {
        // Finn nærmeste UTF-8-grense
        let mut end = max_len;
        while end > 0 && !s.is_char_boundary(end) {
            end -= 1;
        }
        &s[..end]
    }
}
