// synops-notify — Send varsel via epost, WebSocket-push, eller begge.
//
// Løser opp mottaker fra node_id, og sender varsel via valgt kanal:
//   - email: Slår opp epost fra auth_identities, kaller synops-mail
//   - ws: Oppretter notification-node i PG med tilgang for mottaker.
//         PG NOTIFY-trigger sender sanntidsoppdatering via portvokteren.
//   - both: Begge kanaler
//
// Bruk:
//   synops-notify --to <node_id> --message "Tekst" --channel email
//   synops-notify --to <node_id> --message "Tekst" --channel ws
//   synops-notify --to <node_id> --message "Tekst" --channel both
//   synops-notify --to <node_id> --message "Tekst"  (default: ws)
//
// Brukes av orkestreringer og vaktmesteren.
//
// Output: JSON til stdout med resultat per kanal.
// Feil: stderr + exit code != 0.
//
// Ref: docs/retninger/unix_filosofi.md, tools/README.md

use clap::{Parser, ValueEnum};
use serde::Serialize;
use sqlx::PgPool;
use std::process::{Command, Stdio};
use uuid::Uuid;

/// Varslingskanal.
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Channel {
    Email,
    Ws,
    Both,
}

/// Send varsel via epost, WebSocket-push, eller begge.
#[derive(Parser)]
#[command(name = "synops-notify", about = "Send varsel til bruker via epost og/eller WebSocket")]
struct Cli {
    /// Mottaker (node_id for bruker)
    #[arg(long)]
    to: Uuid,

    /// Varseltekst
    #[arg(long)]
    message: String,

    /// Emne for epost (valgfritt, default: "Varsel fra Synops")
    #[arg(long, default_value = "Varsel fra Synops")]
    subject: String,

    /// Kanal: email, ws, both (default: ws)
    #[arg(long, value_enum, default_value = "ws")]
    channel: Channel,

    /// Varslingstype for preferansesjekk (f.eks. "task_assigned", "article_approved").
    /// Hvis satt, sjekkes brukerens metadata.preferences.notifications.<type>.
    #[arg(long)]
    notification_type: Option<String>,

    /// Hopp over preferansesjekk — send uansett brukerens innstillinger.
    #[arg(long)]
    skip_preferences: bool,

    /// Payload fra jobbkø (JSON). Overstyrer andre argumenter.
    #[arg(long)]
    payload_json: Option<String>,
}

/// Resultat fra notifikasjonen.
#[derive(Serialize)]
struct NotifyResult {
    ok: bool,
    to: String,
    channels: Vec<ChannelResult>,
}

#[derive(Serialize)]
struct ChannelResult {
    channel: String,
    ok: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

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

    let cli = Cli::parse();

    // Støtte for --payload-json (jobbkø-dispatch)
    let (to, message, subject, channel, notification_type, skip_preferences) =
        if let Some(ref json_str) = cli.payload_json {
            let payload: serde_json::Value = serde_json::from_str(json_str).unwrap_or_else(|e| {
                eprintln!("Ugyldig --payload-json: {e}");
                std::process::exit(1);
            });
            let to = payload["to"]
                .as_str()
                .and_then(|s| s.parse::<Uuid>().ok())
                .unwrap_or_else(|| {
                    eprintln!("Mangler 'to' (UUID) i payload");
                    std::process::exit(1);
                });
            let message = payload["message"]
                .as_str()
                .unwrap_or_else(|| {
                    eprintln!("Mangler 'message' i payload");
                    std::process::exit(1);
                })
                .to_string();
            let subject = payload["subject"]
                .as_str()
                .unwrap_or("Varsel fra Synops")
                .to_string();
            let channel = match payload["channel"].as_str().unwrap_or("ws") {
                "email" => Channel::Email,
                "both" => Channel::Both,
                _ => Channel::Ws,
            };
            let notification_type = payload["notification_type"]
                .as_str()
                .map(|s| s.to_string());
            let skip_preferences = payload["skip_preferences"].as_bool().unwrap_or(false);
            (to, message, subject, channel, notification_type, skip_preferences)
        } else {
            (
                cli.to,
                cli.message,
                cli.subject,
                cli.channel,
                cli.notification_type,
                cli.skip_preferences,
            )
        };

    let db = synops_common::db::connect().await.unwrap_or_else(|e| {
        eprintln!("{e}");
        std::process::exit(1);
    });

    // Verifiser at mottaker-noden eksisterer, er en bruker/agent, og hent metadata
    let row: Option<(String, serde_json::Value)> = sqlx::query_as(
        "SELECT node_kind::text, metadata FROM nodes WHERE id = $1",
    )
    .bind(to)
    .fetch_optional(&db)
    .await
    .unwrap_or_else(|e| {
        eprintln!("DB-feil ved oppslag av mottaker: {e}");
        std::process::exit(1);
    });

    let (node_kind, metadata) = row.unwrap_or_else(|| {
        eprintln!("Mottaker-node {to} finnes ikke");
        std::process::exit(1);
    });

    if !["person", "agent"].contains(&node_kind.as_str()) {
        eprintln!("Mottaker {to} er {node_kind}, ikke person/agent — kan ikke sende varsel");
        std::process::exit(1);
    }

    // Sjekk brukerens varslingspreferanser (metadata.preferences.notifications)
    // Default: alle kanaler aktivert. Bruker kan skru av per kanal og per type.
    let prefs = if skip_preferences {
        None
    } else {
        get_notification_prefs(&metadata)
    };

    let mut channels = Vec::new();
    let mut all_ok = true;

    // E-post
    if matches!(channel, Channel::Email | Channel::Both) {
        if is_channel_enabled(&prefs, "email", notification_type.as_deref()) {
            let result = send_email(&db, to, &subject, &message).await;
            let ok = result.is_ok();
            if !ok {
                all_ok = false;
            }
            channels.push(ChannelResult {
                channel: "email".to_string(),
                ok,
                detail: result.as_ref().ok().cloned(),
                error: result.err(),
            });
        } else {
            tracing::info!(to = %to, "Epost-varsel hoppet over — deaktivert av bruker");
            channels.push(ChannelResult {
                channel: "email".to_string(),
                ok: true,
                detail: Some("Hoppet over — deaktivert i preferanser".to_string()),
                error: None,
            });
        }
    }

    // WebSocket (via notification-node i PG)
    if matches!(channel, Channel::Ws | Channel::Both) {
        if is_channel_enabled(&prefs, "ws", notification_type.as_deref()) {
            let result = send_ws(&db, to, &subject, &message).await;
            let ok = result.is_ok();
            if !ok {
                all_ok = false;
            }
            channels.push(ChannelResult {
                channel: "ws".to_string(),
                ok,
                detail: result.as_ref().ok().map(|id| format!("notification_node={id}")),
                error: result.err(),
            });
        } else {
            tracing::info!(to = %to, "WS-varsel hoppet over — deaktivert av bruker");
            channels.push(ChannelResult {
                channel: "ws".to_string(),
                ok: true,
                detail: Some("Hoppet over — deaktivert i preferanser".to_string()),
                error: None,
            });
        }
    }

    let output = NotifyResult {
        ok: all_ok,
        to: to.to_string(),
        channels,
    };

    println!("{}", serde_json::to_string_pretty(&output).unwrap());

    if !all_ok {
        std::process::exit(1);
    }
}

/// Hent brukerens varslingspreferanser fra metadata.
///
/// Metadata-struktur (opt-out — alt er aktivert som default):
/// ```json
/// {
///   "preferences": {
///     "notifications": {
///       "email": true,            // global epost-bryter (default: true)
///       "ws": true,               // global ws-bryter (default: true)
///       "task_assigned": true,    // per-type overstyring (default: true)
///       "article_approved": true,
///       "...": true
///     }
///   }
/// }
/// ```
fn get_notification_prefs(metadata: &serde_json::Value) -> Option<serde_json::Value> {
    metadata
        .get("preferences")
        .and_then(|p| p.get("notifications"))
        .cloned()
}

/// Sjekk om en spesifikk kanal er aktivert for brukeren.
///
/// Logikk:
/// 1. Ingen preferanser → aktivert (default)
/// 2. Per-type bryter sjekkes først: `prefs.<notification_type>` = false → deaktivert
/// 3. Global kanal-bryter: `prefs.<channel>` = false → deaktivert
/// 4. Alt annet → aktivert
fn is_channel_enabled(
    prefs: &Option<serde_json::Value>,
    channel_name: &str,
    notification_type: Option<&str>,
) -> bool {
    let prefs = match prefs {
        Some(p) => p,
        None => return true, // Ingen preferanser = alt aktivert
    };

    // Sjekk per-type bryter (overstyrer kanal-bryter)
    if let Some(ntype) = notification_type {
        if let Some(enabled) = prefs.get(ntype).and_then(|v| v.as_bool()) {
            if !enabled {
                return false;
            }
        }
    }

    // Sjekk global kanal-bryter
    prefs
        .get(channel_name)
        .and_then(|v| v.as_bool())
        .unwrap_or(true)
}

/// Send epost via synops-mail. Slår opp mottakers epostadresse fra auth_identities.
async fn send_email(db: &PgPool, to: Uuid, subject: &str, message: &str) -> Result<String, String> {
    let email: Option<String> = sqlx::query_scalar(
        "SELECT email FROM auth_identities WHERE node_id = $1",
    )
    .bind(to)
    .fetch_optional(db)
    .await
    .map_err(|e| format!("DB-feil ved e-post-oppslag: {e}"))?;

    let email = email.ok_or_else(|| format!("Ingen epost registrert for node {to}"))?;

    tracing::info!(to = %email, subject, "Sender epost via synops-mail");

    let output = Command::new("synops-mail")
        .args(["--send", "--to", &email, "--subject", subject, "--body", message])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .map_err(|e| format!("Kunne ikke starte synops-mail: {e}"))?;

    if output.status.success() {
        Ok(format!("Sendt til {email}"))
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        Err(format!("synops-mail feilet: {stderr}"))
    }
}

/// Send WebSocket-varsel ved å opprette en notification-node i PG.
///
/// Steg:
/// 1. Opprett node med node_kind='notification', visibility='private'
/// 2. Opprett node_access-rad for mottaker → noden blir synlig via portvokteren
///
/// PG NOTIFY-triggere (fra migrasjon 018) sender sanntidsoppdatering
/// til portvokteren, som leverer til mottakerens WebSocket-tilkobling.
async fn send_ws(db: &PgPool, to: Uuid, subject: &str, message: &str) -> Result<Uuid, String> {
    let node_id = Uuid::now_v7();

    let metadata = serde_json::json!({
        "notification_type": "general",
        "recipient": to.to_string(),
    });

    // Opprett notification-node og tilgang i én transaksjon
    let mut tx = db.begin().await.map_err(|e| format!("Transaksjon feilet: {e}"))?;

    sqlx::query(
        r#"INSERT INTO nodes (id, node_kind, title, content, visibility, metadata)
           VALUES ($1, 'notification', $2, $3, 'hidden', $4)"#,
    )
    .bind(node_id)
    .bind(subject)
    .bind(message)
    .bind(&metadata)
    .execute(&mut *tx)
    .await
    .map_err(|e| format!("Kunne ikke opprette notification-node: {e}"))?;

    // Gi mottaker lesetilgang — dette trigger access_changed NOTIFY
    sqlx::query(
        r#"INSERT INTO node_access (subject_id, object_id, access)
           VALUES ($1, $2, 'reader')"#,
    )
    .bind(to)
    .bind(node_id)
    .execute(&mut *tx)
    .await
    .map_err(|e| format!("Kunne ikke gi mottaker tilgang: {e}"))?;

    tx.commit().await.map_err(|e| format!("Commit feilet: {e}"))?;

    tracing::info!(
        notification_id = %node_id,
        to = %to,
        "Notification-node opprettet — WebSocket-push via PG NOTIFY"
    );

    Ok(node_id)
}
