// synops-validate — Valider at en node matcher forventet skjema for sin node_kind.
//
// Henter noden fra PostgreSQL og sjekker:
// 1. Basisfelter (visibility, created_by)
// 2. node_kind-spesifikk metadata-validering (collection traits, ai_preset, orchestration, media, communication)
//
// Samler alle avvik i en liste (feiler ikke på første treff).
// Output: avviksliste til stdout (markdown eller JSON).
// Exit-kode: 0 = ingen avvik, 1 = avvik funnet, 2 = node finnes ikke / feil.
//
// Brukes av valideringsfasen og som pre-commit sjekk.
//
// Miljøvariabler:
//   DATABASE_URL — PostgreSQL-tilkobling (påkrevd)
//
// Ref: docs/primitiver/nodes.md, docs/primitiver/traits.md

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

/// Valider at en node matcher forventet skjema for sin node_kind.
#[derive(Parser)]
#[command(name = "synops-validate", about = "Valider node-skjema")]
struct Cli {
    /// Node-ID (UUID)
    #[arg(long)]
    node_id: Uuid,

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

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

#[derive(Debug, Clone, Serialize)]
struct Violation {
    field: String,
    message: String,
    severity: Severity,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "lowercase")]
enum Severity {
    Error,
    Warning,
}

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

// --- Kjente verdier (speiler maskinrommet/src/intentions.rs) ---

const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "open"];

const VALID_NODE_KINDS: &[&str] = &[
    "person", "team", "collection", "content", "communication",
    "topic", "media", "agent", "system_announcement", "ai_preset",
    "workspace", "work_item", "cli_tool", "orchestration",
];

const VALID_TRAITS: &[&str] = &[
    // Innhold & redigering
    "editor", "versioning", "collaboration", "translation", "templates",
    // Publisering & distribusjon
    "publishing", "rss", "newsletter", "custom_domain", "analytics", "embed", "api",
    // Lyd & video
    "podcast", "recording", "transcription", "tts", "clips", "playlist", "mixer", "studio",
    // Kommunikasjon
    "chat", "forum", "comments", "guest_input", "announcements", "polls", "qa",
    // Organisering
    "kanban", "calendar", "timeline", "table", "gallery", "bookmarks", "tags",
    // Kunnskap
    "knowledge_graph", "mindmap", "wiki", "glossary", "faq", "bibliography",
    // Automatisering & AI
    "auto_tag", "auto_summarize", "digest", "bridge", "moderation", "ai_tool", "orchestration",
    // Tilgang & fellesskap
    "membership", "roles", "invites", "paywall", "directory",
    // Ekstern integrasjon
    "webhook", "import", "export", "ical_sync",
];

const VALID_TRIGGER_EVENTS: &[&str] = &[
    "node.created", "edge.created", "communication.ended",
    "node.published", "scheduled.due", "manual",
];

const VALID_EXECUTORS: &[&str] = &["script", "bot", "dream"];

const VALID_MODEL_PROFILES: &[&str] = &["flash", "standard"];

const VALID_AI_PRESET_CATEGORIES: &[&str] = &["standard", "custom"];

const VALID_AI_PRESET_DIRECTIONS: &[&str] = &["tool_to_node", "node_to_tool", "both"];

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

    let cli = Cli::parse();

    match run(cli).await {
        Ok(violations) => {
            process::exit(if violations.is_empty() { 0 } else { 1 });
        }
        Err(e) => {
            eprintln!("Feil: {e}");
            process::exit(2);
        }
    }
}

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

    let node = sqlx::query_as::<_, NodeRow>(
        "SELECT id, node_kind::text, title, content, visibility::text, \
         metadata, created_at, created_by FROM nodes WHERE id = $1",
    )
    .bind(cli.node_id)
    .fetch_optional(&db)
    .await
    .map_err(|e| format!("DB-feil: {e}"))?
    .ok_or_else(|| format!("Node {} finnes ikke", cli.node_id))?;

    let mut violations = Vec::new();

    // 1. Basisvalidering
    validate_base(&node, &mut violations);

    // 2. node_kind-spesifikk validering
    match node.node_kind.as_str() {
        "collection" => validate_collection(&node.metadata, &mut violations),
        "ai_preset" => validate_ai_preset(&node.metadata, &mut violations),
        "orchestration" => validate_orchestration(&node.metadata, &mut violations),
        "media" => validate_media(&node.metadata, &mut violations),
        "communication" => validate_communication(&node.metadata, &mut violations),
        "person" => validate_person(&node, &db, &mut violations).await,
        "agent" => validate_agent(&node, &db, &mut violations).await,
        _ => {} // content, topic, team, workspace, work_item, cli_tool, system_announcement — ingen spesifikk validering
    }

    let node_kind = &node.node_kind;
    let node_id = node.id;
    let title = node.title.as_deref().unwrap_or("Uten tittel");

    // Output
    match cli.format {
        OutputFormat::Json => {
            #[derive(Serialize)]
            struct Report<'a> {
                node_id: Uuid,
                node_kind: &'a str,
                title: &'a str,
                violations: &'a [Violation],
                valid: bool,
            }
            let report = Report {
                node_id,
                node_kind,
                title,
                valid: violations.is_empty(),
                violations: &violations,
            };
            println!(
                "{}",
                serde_json::to_string_pretty(&report)
                    .map_err(|e| format!("JSON-feil: {e}"))?
            );
        }
        OutputFormat::Md => {
            if violations.is_empty() {
                println!("OK: {} (`{}`, {}) — ingen avvik", title, node_kind, node_id);
            } else {
                println!("# Valideringsavvik: {} (`{}`)\n", title, node_kind);
                println!("Node: `{}`\n", node_id);
                println!("| # | Alvorlighet | Felt | Avvik |");
                println!("|---|-------------|------|-------|");
                for (i, v) in violations.iter().enumerate() {
                    let sev = match v.severity {
                        Severity::Error => "FEIL",
                        Severity::Warning => "ADVARSEL",
                    };
                    println!("| {} | {} | `{}` | {} |", i + 1, sev, v.field, v.message);
                }
                println!("\nTotalt {} avvik.", violations.len());
            }
        }
    }

    tracing::info!(
        node_id = %node_id,
        node_kind = %node_kind,
        violations = violations.len(),
        "Validering fullført"
    );

    Ok(violations)
}

// =============================================================================
// Basisvalidering (alle node_kinds)
// =============================================================================

fn validate_base(node: &NodeRow, violations: &mut Vec<Violation>) {
    // Sjekk at node_kind er kjent
    if !VALID_NODE_KINDS.contains(&node.node_kind.as_str()) {
        violations.push(Violation {
            field: "node_kind".into(),
            message: format!("Ukjent node_kind: '{}'. Kjente: {:?}", node.node_kind, VALID_NODE_KINDS),
            severity: Severity::Warning,
        });
    }

    // Sjekk visibility
    if !VALID_VISIBILITIES.contains(&node.visibility.as_str()) {
        violations.push(Violation {
            field: "visibility".into(),
            message: format!("Ugyldig visibility: '{}'. Gyldige: {:?}", node.visibility, VALID_VISIBILITIES),
            severity: Severity::Error,
        });
    }

    // created_by bør være satt (advarsel, ikke feil — system-noder kan mangle)
    if node.created_by.is_none() {
        violations.push(Violation {
            field: "created_by".into(),
            message: "created_by er NULL".into(),
            severity: Severity::Warning,
        });
    }

    // metadata bør være et objekt
    if !node.metadata.is_object() {
        violations.push(Violation {
            field: "metadata".into(),
            message: format!("metadata er ikke et JSON-objekt (type: {})", json_type_name(&node.metadata)),
            severity: Severity::Error,
        });
    }
}

// =============================================================================
// Collection: traits-validering
// =============================================================================

fn validate_collection(metadata: &serde_json::Value, violations: &mut Vec<Violation>) {
    let traits = match metadata.get("traits") {
        None => return, // Ingen traits er OK
        Some(t) => t,
    };

    let traits_obj = match traits.as_object() {
        None => {
            violations.push(Violation {
                field: "metadata.traits".into(),
                message: "traits må være et objekt".into(),
                severity: Severity::Error,
            });
            return;
        }
        Some(o) => o,
    };

    // Sjekk ukjente trait-navn
    for key in traits_obj.keys() {
        if !VALID_TRAITS.contains(&key.as_str()) {
            violations.push(Violation {
                field: format!("metadata.traits.{key}"),
                message: format!("Ukjent trait: '{key}'"),
                severity: Severity::Error,
            });
        }
    }

    // mindmap-konfigurasjon
    if let Some(mindmap) = traits_obj.get("mindmap") {
        if let Some(depth) = mindmap.get("default_depth") {
            match depth.as_i64() {
                Some(d) if !(1..=3).contains(&d) => {
                    violations.push(Violation {
                        field: "metadata.traits.mindmap.default_depth".into(),
                        message: format!("Må være 1–3, fikk {d}"),
                        severity: Severity::Error,
                    });
                }
                None => {
                    violations.push(Violation {
                        field: "metadata.traits.mindmap.default_depth".into(),
                        message: "Må være et heltall".into(),
                        severity: Severity::Error,
                    });
                }
                _ => {}
            }
        }
        if let Some(layout) = mindmap.get("layout") {
            match layout.as_str() {
                Some(l) if l != "radial" && l != "tree" => {
                    violations.push(Violation {
                        field: "metadata.traits.mindmap.layout".into(),
                        message: format!("Må være 'radial' eller 'tree', fikk '{l}'"),
                        severity: Severity::Error,
                    });
                }
                None => {
                    violations.push(Violation {
                        field: "metadata.traits.mindmap.layout".into(),
                        message: "Må være en streng".into(),
                        severity: Severity::Error,
                    });
                }
                _ => {}
            }
        }
    }
}

// =============================================================================
// AI Preset
// =============================================================================

fn validate_ai_preset(metadata: &serde_json::Value, violations: &mut Vec<Violation>) {
    // Påkrevde streng-felter
    check_required_str(metadata, "prompt", violations);
    check_required_enum(metadata, "model_profile", VALID_MODEL_PROFILES, violations);
    check_required_enum(metadata, "category", VALID_AI_PRESET_CATEGORIES, violations);
    check_required_enum(metadata, "default_direction", VALID_AI_PRESET_DIRECTIONS, violations);
    check_required_str(metadata, "icon", violations);

    // color: hex-farge
    match metadata.get("color").and_then(|v| v.as_str()) {
        None | Some("") => {
            violations.push(Violation {
                field: "metadata.color".into(),
                message: "Påkrevd: hex-farge (#RRGGBB)".into(),
                severity: Severity::Error,
            });
        }
        Some(c) if !c.starts_with('#') || c.len() != 7 => {
            violations.push(Violation {
                field: "metadata.color".into(),
                message: format!("Ugyldig hex-farge: '{c}'. Forventet #RRGGBB"),
                severity: Severity::Error,
            });
        }
        _ => {}
    }
}

// =============================================================================
// Orchestration
// =============================================================================

fn validate_orchestration(metadata: &serde_json::Value, violations: &mut Vec<Violation>) {
    // trigger (objekt)
    match metadata.get("trigger") {
        None => {
            violations.push(Violation {
                field: "metadata.trigger".into(),
                message: "Påkrevd: trigger-objekt".into(),
                severity: Severity::Error,
            });
        }
        Some(trigger) => {
            match trigger.as_object() {
                None => {
                    violations.push(Violation {
                        field: "metadata.trigger".into(),
                        message: "Må være et objekt".into(),
                        severity: Severity::Error,
                    });
                }
                Some(trigger_obj) => {
                    // trigger.event
                    match trigger_obj.get("event").and_then(|v| v.as_str()) {
                        None | Some("") => {
                            violations.push(Violation {
                                field: "metadata.trigger.event".into(),
                                message: "Påkrevd: trigger-event".into(),
                                severity: Severity::Error,
                            });
                        }
                        Some(ev) if !VALID_TRIGGER_EVENTS.contains(&ev) => {
                            violations.push(Violation {
                                field: "metadata.trigger.event".into(),
                                message: format!("Ukjent event: '{ev}'. Gyldige: {VALID_TRIGGER_EVENTS:?}"),
                                severity: Severity::Error,
                            });
                        }
                        _ => {}
                    }

                    // trigger.conditions (valgfri, men må være objekt)
                    if let Some(cond) = trigger_obj.get("conditions") {
                        if !cond.is_object() {
                            violations.push(Violation {
                                field: "metadata.trigger.conditions".into(),
                                message: "Må være et objekt".into(),
                                severity: Severity::Error,
                            });
                        }
                    }
                }
            }
        }
    }

    // executor
    check_required_enum(metadata, "executor", VALID_EXECUTORS, violations);

    // intelligence (valgfri, 1–3)
    if let Some(val) = metadata.get("intelligence") {
        match val.as_i64() {
            Some(n) if !(1..=3).contains(&n) => {
                violations.push(Violation {
                    field: "metadata.intelligence".into(),
                    message: format!("Må være 1–3, fikk {n}"),
                    severity: Severity::Error,
                });
            }
            None => {
                violations.push(Violation {
                    field: "metadata.intelligence".into(),
                    message: "Må være et heltall".into(),
                    severity: Severity::Error,
                });
            }
            _ => {}
        }
    }

    // effort (valgfri, 1–3)
    if let Some(val) = metadata.get("effort") {
        match val.as_i64() {
            Some(n) if !(1..=3).contains(&n) => {
                violations.push(Violation {
                    field: "metadata.effort".into(),
                    message: format!("Må være 1–3, fikk {n}"),
                    severity: Severity::Error,
                });
            }
            None => {
                violations.push(Violation {
                    field: "metadata.effort".into(),
                    message: "Må være et heltall".into(),
                    severity: Severity::Error,
                });
            }
            _ => {}
        }
    }

    // compiled (valgfri, boolean)
    if let Some(val) = metadata.get("compiled") {
        if !val.is_boolean() {
            violations.push(Violation {
                field: "metadata.compiled".into(),
                message: "Må være en boolean".into(),
                severity: Severity::Error,
            });
        }
    }

    // pipeline (valgfri, array)
    if let Some(val) = metadata.get("pipeline") {
        if !val.is_array() {
            violations.push(Violation {
                field: "metadata.pipeline".into(),
                message: "Må være et array".into(),
                severity: Severity::Error,
            });
        }
    }
}

// =============================================================================
// Media
// =============================================================================

fn validate_media(metadata: &serde_json::Value, violations: &mut Vec<Violation>) {
    check_required_str(metadata, "cas_hash", violations);
    check_required_str(metadata, "mime", violations);

    match metadata.get("size_bytes") {
        None => {
            violations.push(Violation {
                field: "metadata.size_bytes".into(),
                message: "Påkrevd: filstørrelse i bytes".into(),
                severity: Severity::Error,
            });
        }
        Some(v) if !v.is_i64() && !v.is_u64() => {
            violations.push(Violation {
                field: "metadata.size_bytes".into(),
                message: "Må være et heltall".into(),
                severity: Severity::Error,
            });
        }
        _ => {}
    }
}

// =============================================================================
// Communication
// =============================================================================

fn validate_communication(metadata: &serde_json::Value, violations: &mut Vec<Violation>) {
    // started_at bør finnes (settes automatisk av create_node)
    if metadata.get("started_at").is_none() {
        violations.push(Violation {
            field: "metadata.started_at".into(),
            message: "Mangler started_at (bør settes automatisk)".into(),
            severity: Severity::Warning,
        });
    }

    // ended_at (valgfri, men bør være streng hvis satt)
    if let Some(ended) = metadata.get("ended_at") {
        if !ended.is_string() {
            violations.push(Violation {
                field: "metadata.ended_at".into(),
                message: "Må være en ISO8601-streng".into(),
                severity: Severity::Error,
            });
        }
    }
}

// =============================================================================
// Person — bør ha auth_identity
// =============================================================================

async fn validate_person(node: &NodeRow, db: &sqlx::PgPool, violations: &mut Vec<Violation>) {
    let has_auth = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS(SELECT 1 FROM auth_identities WHERE node_id = $1)",
    )
    .bind(node.id)
    .fetch_one(db)
    .await;

    match has_auth {
        Ok(false) => {
            violations.push(Violation {
                field: "auth_identities".into(),
                message: "Person-node mangler auth_identity-kobling".into(),
                severity: Severity::Warning,
            });
        }
        Err(e) => {
            tracing::warn!("Kunne ikke sjekke auth_identities: {e}");
        }
        _ => {}
    }

    // title bør være satt (visnavn)
    if node.title.is_none() {
        violations.push(Violation {
            field: "title".into(),
            message: "Person-node bør ha title (visnavn)".into(),
            severity: Severity::Warning,
        });
    }
}

// =============================================================================
// Agent — bør ha agent_identity
// =============================================================================

async fn validate_agent(node: &NodeRow, db: &sqlx::PgPool, violations: &mut Vec<Violation>) {
    let has_agent = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS(SELECT 1 FROM agent_identities WHERE node_id = $1)",
    )
    .bind(node.id)
    .fetch_one(db)
    .await;

    match has_agent {
        Ok(false) => {
            violations.push(Violation {
                field: "agent_identities".into(),
                message: "Agent-node mangler agent_identity-kobling".into(),
                severity: Severity::Warning,
            });
        }
        Err(e) => {
            tracing::warn!("Kunne ikke sjekke agent_identities: {e}");
        }
        _ => {}
    }
}

// =============================================================================
// Hjelpefunksjoner
// =============================================================================

fn check_required_str(metadata: &serde_json::Value, field: &str, violations: &mut Vec<Violation>) {
    match metadata.get(field).and_then(|v| v.as_str()) {
        None | Some("") => {
            violations.push(Violation {
                field: format!("metadata.{field}"),
                message: format!("Påkrevd: ikke-tom streng"),
                severity: Severity::Error,
            });
        }
        _ => {}
    }
}

fn check_required_enum(
    metadata: &serde_json::Value,
    field: &str,
    valid: &[&str],
    violations: &mut Vec<Violation>,
) {
    match metadata.get(field).and_then(|v| v.as_str()) {
        None => {
            violations.push(Violation {
                field: format!("metadata.{field}"),
                message: format!("Påkrevd. Gyldige verdier: {valid:?}"),
                severity: Severity::Error,
            });
        }
        Some(v) if !valid.contains(&v) => {
            violations.push(Violation {
                field: format!("metadata.{field}"),
                message: format!("Ugyldig verdi: '{v}'. Gyldige: {valid:?}"),
                severity: Severity::Error,
            });
        }
        _ => {}
    }
}

fn json_type_name(v: &serde_json::Value) -> &'static str {
    match v {
        serde_json::Value::Null => "null",
        serde_json::Value::Bool(_) => "boolean",
        serde_json::Value::Number(_) => "number",
        serde_json::Value::String(_) => "string",
        serde_json::Value::Array(_) => "array",
        serde_json::Value::Object(_) => "object",
    }
}
