//! Daemon/vaktmester-modus for synops-agent.
//!
//! Kjører som bakgrunnsprosess:
//! - Poller task-noder fra PG med konfigurerbart intervall
//! - Mottar meldinger fra vaktmester-chat (kommunikasjonsnode)
//! - Prefix-kommandoer: /proposal, /task, /bug, /gjør
//! - Modellvalg: /claude, /grok, /gemini, /lokal, /billig (kun admin)
//! - Spawn Claude Code for tunge oppgaver
//! - Heartbeat-fil for overvåking
//! - PID-fil + SIGTERM-håndtering

use chrono::Utc;
use sqlx::PgPool;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use uuid::Uuid;

use crate::graph;
use crate::provider::{ApiKeys, create_provider};

/// Daemon configuration.
pub struct DaemonConfig {
    /// Poll interval in seconds.
    pub interval_secs: u64,
    /// Default model spec (e.g. "openrouter/anthropic/claude-sonnet-4").
    pub default_model: String,
    /// Working directory for agent sessions.
    pub working_dir: PathBuf,
    /// Path for heartbeat file.
    pub heartbeat_path: PathBuf,
    /// Path for PID file.
    pub pid_path: PathBuf,
    /// Max cost per task in USD.
    pub max_cost_per_task: Option<f64>,
    /// Max iterations per task.
    pub max_iterations: usize,
    /// Verbose logging.
    pub verbose: bool,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        Self {
            interval_secs: 30,
            default_model: "openrouter/anthropic/claude-sonnet-4".into(),
            working_dir: PathBuf::from("/home/vegard/synops"),
            heartbeat_path: PathBuf::from("/tmp/synops-agent-daemon.heartbeat"),
            pid_path: PathBuf::from("/tmp/synops-agent-daemon.pid"),
            max_cost_per_task: Some(0.50),
            max_iterations: 50,
            verbose: false,
        }
    }
}

/// Run the daemon loop.
pub async fn run(pool: PgPool, config: DaemonConfig) -> Result<(), Box<dyn std::error::Error>> {
    // Write PID file
    let pid = std::process::id();
    std::fs::write(&config.pid_path, pid.to_string())?;
    tracing::info!(pid, pid_file = %config.pid_path.display(), "PID-fil skrevet");

    // Set up shutdown signal
    let shutdown = Arc::new(AtomicBool::new(false));
    let shutdown_clone = shutdown.clone();

    // SIGTERM via tokio signal
    tokio::spawn(async move {
        let _ = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .expect("kunne ikke lytte på SIGTERM")
            .recv()
            .await;
        tracing::info!("SIGTERM mottatt — avslutter daemon");
        shutdown_clone.store(true, Ordering::SeqCst);
    });

    // SIGINT (Ctrl+C)
    let shutdown_clone2 = shutdown.clone();
    tokio::spawn(async move {
        let _ = tokio::signal::ctrl_c().await;
        tracing::info!("SIGINT mottatt — avslutter daemon");
        shutdown_clone2.store(true, Ordering::SeqCst);
    });

    // Find or create vaktmester chat
    let vaktmester_chat_id = graph::find_or_create_vaktmester_chat(&pool).await?;
    tracing::info!(chat_id = %vaktmester_chat_id, "Vaktmester-chat aktiv");

    let mut last_poll = Utc::now();
    let api_keys = ApiKeys::from_env();

    tracing::info!(
        interval = config.interval_secs,
        model = config.default_model.as_str(),
        "Daemon startet"
    );

    write_heartbeat(&config.heartbeat_path);

    loop {
        if shutdown.load(Ordering::SeqCst) {
            break;
        }

        // Check kill switch
        match graph::is_agent_active(&pool).await {
            Ok(false) => {
                tracing::warn!("Agent deaktivert (kill switch) — venter");
                tokio::time::sleep(tokio::time::Duration::from_secs(config.interval_secs)).await;
                write_heartbeat(&config.heartbeat_path);
                continue;
            }
            Err(e) => {
                tracing::warn!(error = %e, "Kunne ikke sjekke kill switch");
            }
            _ => {}
        }

        // 1. Poll vaktmester-chat for new messages
        match graph::poll_chat_messages(&pool, vaktmester_chat_id, last_poll).await {
            Ok(messages) if !messages.is_empty() => {
                tracing::info!(count = messages.len(), "Nye meldinger i vaktmester-chat");
                for msg in &messages {
                    if let Err(e) = handle_chat_message(
                        &pool,
                        vaktmester_chat_id,
                        msg,
                        &config,
                        &api_keys,
                    )
                    .await
                    {
                        tracing::error!(error = %e, msg_id = %msg.id, "Feil ved behandling av melding");
                        let _ = graph::write_chat_reply(
                            &pool,
                            vaktmester_chat_id,
                            &format!("Feil: {e}"),
                        )
                        .await;
                    }
                }
                // Update last_poll to latest message time
                if let Some(latest) = messages.last() {
                    last_poll = latest.created_at;
                }
            }
            Ok(_) => {} // no new messages
            Err(e) => {
                tracing::warn!(error = %e, "Chat-polling feilet");
            }
        }

        // 2. Poll for open tasks
        match graph::pick_task(&pool).await {
            Ok(Some(task)) => {
                tracing::info!(
                    task_id = %task.id,
                    title = task.title.as_deref().unwrap_or("(uten tittel)"),
                    "Plukket task"
                );
                run_task(&pool, &task, &config, &api_keys).await;
            }
            Ok(None) => {} // no tasks
            Err(e) => {
                tracing::warn!(error = %e, "Task-polling feilet");
            }
        }

        // 3. Release stale tasks
        match graph::release_stale_tasks(&pool).await {
            Ok(0) => {}
            Ok(n) => tracing::warn!(count = n, "Frigjorde stale tasks"),
            Err(e) => tracing::warn!(error = %e, "Stale-task frigjøring feilet"),
        }

        write_heartbeat(&config.heartbeat_path);

        // Sleep with shutdown check
        for _ in 0..config.interval_secs {
            if shutdown.load(Ordering::SeqCst) {
                break;
            }
            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
        }
    }

    // Cleanup
    tracing::info!("Daemon avslutter — rydder opp");
    let _ = std::fs::remove_file(&config.pid_path);
    let _ = std::fs::remove_file(&config.heartbeat_path);

    Ok(())
}

/// Handle a single chat message: parse prefix commands or respond with LLM.
async fn handle_chat_message(
    pool: &PgPool,
    chat_id: Uuid,
    msg: &graph::ChatMessage,
    config: &DaemonConfig,
    api_keys: &ApiKeys,
) -> Result<(), Box<dyn std::error::Error>> {
    let text = msg.content.trim();
    tracing::info!(from = ?msg.created_by, "Melding: {}", truncate(text, 100));

    // Parse prefix commands
    if let Some(rest) = text.strip_prefix("/proposal ") {
        return handle_proposal(pool, chat_id, rest.trim()).await;
    }
    if let Some(rest) = text.strip_prefix("/task ") {
        return handle_task_create(pool, chat_id, rest.trim()).await;
    }
    if let Some(rest) = text.strip_prefix("/bug ") {
        return handle_bug(pool, chat_id, rest.trim()).await;
    }
    if let Some(rest) = text.strip_prefix("/gjør ") {
        return handle_do(pool, chat_id, rest.trim(), &config.default_model, config, api_keys).await;
    }

    // Model-selection commands (admin only — all messages in vaktmester chat are admin)
    if let Some(rest) = text.strip_prefix("/claude ") {
        return handle_claude_code(pool, chat_id, rest.trim(), config).await;
    }
    if let Some(rest) = text.strip_prefix("/grok ") {
        return handle_do(pool, chat_id, rest.trim(), "xai/grok-3", config, api_keys).await;
    }
    if let Some(rest) = text.strip_prefix("/gemini ") {
        return handle_do(pool, chat_id, rest.trim(), "gemini/gemini-2.5-flash", config, api_keys).await;
    }
    if let Some(rest) = text.strip_prefix("/lokal ") {
        return handle_do(pool, chat_id, rest.trim(), "ollama/llama3", config, api_keys).await;
    }
    if let Some(rest) = text.strip_prefix("/billig ") {
        return handle_do(pool, chat_id, rest.trim(), "gemini/gemini-2.5-flash", config, api_keys).await;
    }

    // Free text — respond with default model
    handle_do(pool, chat_id, text, &config.default_model, config, api_keys).await
}

/// /proposal — create a proposal node.
async fn handle_proposal(
    pool: &PgPool,
    chat_id: Uuid,
    description: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    // Use first line as title, rest as body
    let (title, body) = split_title_body(description);
    let id = graph::create_proposal(pool, &title, &body).await?;
    let reply = format!("Proposal opprettet: {} ({})", title, short_id(id));
    graph::write_chat_reply(pool, chat_id, &reply).await?;
    tracing::info!(id = %id, title = title.as_str(), "Proposal opprettet");
    Ok(())
}

/// /task — create a task node.
async fn handle_task_create(
    pool: &PgPool,
    chat_id: Uuid,
    description: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let (title, body) = split_title_body(description);
    let id = graph::create_task(pool, &title, &body, 5).await?;
    let reply = format!("Task opprettet: {} ({})", title, short_id(id));
    graph::write_chat_reply(pool, chat_id, &reply).await?;
    tracing::info!(id = %id, title = title.as_str(), "Task opprettet");
    Ok(())
}

/// /bug — add to fikseliste (as task with high priority).
async fn handle_bug(
    pool: &PgPool,
    chat_id: Uuid,
    description: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let (title, body) = split_title_body(description);
    let title = format!("[BUG] {}", title);
    let id = graph::create_task(pool, &title, &body, 2).await?;
    let reply = format!("Bug registrert: {} ({})", title, short_id(id));
    graph::write_chat_reply(pool, chat_id, &reply).await?;
    tracing::info!(id = %id, "Bug registrert");
    Ok(())
}

/// /gjør or free text — run with an LLM.
async fn handle_do(
    pool: &PgPool,
    chat_id: Uuid,
    task: &str,
    model: &str,
    config: &DaemonConfig,
    api_keys: &ApiKeys,
) -> Result<(), Box<dyn std::error::Error>> {
    let reply_start = format!("Kjører med {} ...", short_model(model));
    graph::write_chat_reply(pool, chat_id, &reply_start).await?;

    // Create a one-off agent session for this task
    let provider = create_provider(model, api_keys, None)?;

    let system_prompt = crate::build_system_prompt(None, &config.working_dir).await;
    let messages = vec![
        crate::provider::Message {
            role: "system".into(),
            content: Some(system_prompt),
            tool_calls: None,
            tool_call_id: None,
        },
        crate::provider::Message {
            role: "user".into(),
            content: Some(task.to_string()),
            tool_calls: None,
            tool_call_id: None,
        },
    ];

    let compaction_config = crate::context::CompactionConfig {
        context_window: provider.context_window(),
        ..Default::default()
    };

    let mut session = crate::AgentSession {
        provider,
        messages,
        tool_defs: crate::tools::tool_definitions(),
        total_usage: std::collections::HashMap::new(),
        total_cost: 0.0,
        total_iterations: 0,
        retry_config: crate::provider::RetryConfig {
            max_retries: 3,
            ..Default::default()
        },
        compaction_config,
        working_dir: config.working_dir.clone(),
        max_iterations: config.max_iterations,
        max_cost: config.max_cost_per_task,
        verbose: config.verbose,
        interrupted: Arc::new(AtomicBool::new(false)),
        pg_pool: Some(pool.clone()),
        session_id: crate::checkpoint::new_session_id(),
        model_spec: model.to_string(),
        task_text: Some(task.to_string()),
        task_node_id: None,
        checkpoint_interval: 10,
        tool_calls_since_checkpoint: 0,
        started_at: Utc::now(),
    };

    let result = session.run_turn().await;

    // Finalize: log usage to PG
    let status = match &result {
        Ok(crate::TurnResult::Done) => "completed",
        Ok(crate::TurnResult::BudgetExhausted) => "budget_exhausted",
        Ok(crate::TurnResult::MaxIterations) => "max_iterations",
        Ok(crate::TurnResult::Interrupted) => "interrupted",
        Err(_) => "failed",
    };
    session.finalize(status).await;

    // Extract the last assistant text as the reply
    let response_text = session
        .messages
        .iter()
        .rev()
        .find(|m| m.role == "assistant" && m.content.is_some())
        .and_then(|m| m.content.clone())
        .unwrap_or_else(|| "(ingen respons)".into());

    let cost_info = format!(" (${:.4})", session.total_cost);

    match result {
        Ok(crate::TurnResult::Done) => {
            let reply = format!("{}\n\n_Ferdig{}_", response_text, cost_info);
            graph::write_chat_reply(pool, chat_id, &reply).await?;
        }
        Ok(crate::TurnResult::BudgetExhausted) => {
            let reply = format!(
                "{}\n\n_Budsjettgrense nådd{}. Oppgaven er ikke fullført._",
                response_text, cost_info
            );
            graph::write_chat_reply(pool, chat_id, &reply).await?;
        }
        Ok(crate::TurnResult::MaxIterations) => {
            let reply = format!(
                "{}\n\n_Maks iterasjoner nådd{}._",
                response_text, cost_info
            );
            graph::write_chat_reply(pool, chat_id, &reply).await?;
        }
        Ok(crate::TurnResult::Interrupted) => {
            graph::write_chat_reply(pool, chat_id, "Avbrutt.").await?;
        }
        Err(e) => {
            let reply = format!("Feil: {}", e);
            graph::write_chat_reply(pool, chat_id, &reply).await?;
        }
    }

    Ok(())
}

/// /claude — spawn Claude Code for heavy tasks.
async fn handle_claude_code(
    pool: &PgPool,
    chat_id: Uuid,
    task: &str,
    config: &DaemonConfig,
) -> Result<(), Box<dyn std::error::Error>> {
    graph::write_chat_reply(pool, chat_id, "Spawner Claude Code ...").await?;

    match crate::spawn_claude_code(task, &config.working_dir).await {
        Ok(()) => {
            graph::write_chat_reply(pool, chat_id, "Claude Code ferdig.").await?;
        }
        Err(e) => {
            graph::write_chat_reply(pool, chat_id, &format!("Claude Code feilet: {e}")).await?;
        }
    }

    Ok(())
}

/// Run a picked task through an agent session.
async fn run_task(
    pool: &PgPool,
    task: &graph::TaskInfo,
    config: &DaemonConfig,
    api_keys: &ApiKeys,
) {
    let task_desc = task
        .content
        .as_deref()
        .or(task.title.as_deref())
        .unwrap_or("(ingen beskrivelse)");

    // Report start
    let _ = graph::write_task_message(pool, task.id, &format!("Starter: {}", truncate(task_desc, 200))).await;

    let provider = match create_provider(&config.default_model, api_keys, None) {
        Ok(p) => p,
        Err(e) => {
            tracing::error!(error = %e, "Kunne ikke opprette provider for task");
            let _ = graph::update_task_status(pool, task.id, "failed", Some(&format!("Provider-feil: {e}"))).await;
            return;
        }
    };

    let system_prompt = crate::build_system_prompt(None, &config.working_dir).await;
    let messages = vec![
        crate::provider::Message {
            role: "system".into(),
            content: Some(system_prompt),
            tool_calls: None,
            tool_call_id: None,
        },
        crate::provider::Message {
            role: "user".into(),
            content: Some(task_desc.to_string()),
            tool_calls: None,
            tool_call_id: None,
        },
    ];

    let compaction_config = crate::context::CompactionConfig {
        context_window: provider.context_window(),
        ..Default::default()
    };

    let mut session = crate::AgentSession {
        provider,
        messages,
        tool_defs: crate::tools::tool_definitions(),
        total_usage: std::collections::HashMap::new(),
        total_cost: 0.0,
        total_iterations: 0,
        retry_config: crate::provider::RetryConfig {
            max_retries: 3,
            ..Default::default()
        },
        compaction_config,
        working_dir: config.working_dir.clone(),
        max_iterations: config.max_iterations,
        max_cost: config.max_cost_per_task,
        verbose: config.verbose,
        interrupted: Arc::new(AtomicBool::new(false)),
        pg_pool: Some(pool.clone()),
        session_id: crate::checkpoint::new_session_id(),
        model_spec: config.default_model.clone(),
        task_text: Some(task_desc.to_string()),
        task_node_id: Some(task.id.to_string()),
        checkpoint_interval: 10,
        tool_calls_since_checkpoint: 0,
        started_at: Utc::now(),
    };

    let result = session.run_turn().await;

    // Finalize: checkpoint + PG logging
    let status = match &result {
        Ok(crate::TurnResult::Done) => "completed",
        Ok(crate::TurnResult::BudgetExhausted) => "budget_exhausted",
        Ok(crate::TurnResult::MaxIterations) => "max_iterations",
        Ok(crate::TurnResult::Interrupted) => "interrupted",
        Err(_) => "failed",
    };
    session.finalize(status).await;

    let summary = session
        .messages
        .iter()
        .rev()
        .find(|m| m.role == "assistant" && m.content.is_some())
        .and_then(|m| m.content.clone())
        .unwrap_or_default();

    let cost_str = format!("${:.4}", session.total_cost);

    match result {
        Ok(crate::TurnResult::Done) => {
            let result_msg = format!("Ferdig ({}). {}", cost_str, truncate(&summary, 500));
            let _ = graph::update_task_status(pool, task.id, "done", Some(&result_msg)).await;
            let _ = graph::write_task_message(pool, task.id, &result_msg).await;
            tracing::info!(task_id = %task.id, cost = cost_str.as_str(), "Task ferdig");
        }
        Ok(crate::TurnResult::BudgetExhausted) => {
            let result_msg = format!("Budsjett brukt opp ({}). Delvis ferdig.", cost_str);
            let _ = graph::update_task_status(pool, task.id, "failed", Some(&result_msg)).await;
            let _ = graph::write_task_message(pool, task.id, &result_msg).await;
            tracing::warn!(task_id = %task.id, "Task: budsjett oppbrukt");
        }
        Ok(crate::TurnResult::MaxIterations) => {
            let result_msg = format!("Maks iterasjoner nådd ({}). Delvis ferdig.", cost_str);
            let _ = graph::update_task_status(pool, task.id, "failed", Some(&result_msg)).await;
            let _ = graph::write_task_message(pool, task.id, &result_msg).await;
        }
        Ok(crate::TurnResult::Interrupted) => {
            let _ = graph::update_task_status(pool, task.id, "failed", Some("Avbrutt")).await;
        }
        Err(e) => {
            let result_msg = format!("Feil: {}", e);
            let _ = graph::update_task_status(pool, task.id, "failed", Some(&result_msg)).await;
            let _ = graph::write_task_message(pool, task.id, &result_msg).await;
            tracing::error!(task_id = %task.id, error = %e, "Task feilet");
        }
    }
}

/// Write heartbeat timestamp to file.
fn write_heartbeat(path: &Path) {
    let _ = std::fs::write(path, Utc::now().to_rfc3339());
}

/// Split "first line\nrest" into (title, body).
fn split_title_body(text: &str) -> (String, String) {
    if let Some(newline) = text.find('\n') {
        let title = text[..newline].trim().to_string();
        let body = text[newline + 1..].trim().to_string();
        (title, body)
    } else {
        (text.trim().to_string(), String::new())
    }
}

/// Short model name for display.
fn short_model(model: &str) -> &str {
    model.rsplit('/').next().unwrap_or(model)
}

/// Truncate string for display.
fn truncate(s: &str, max: usize) -> &str {
    if s.len() > max {
        // Find char boundary
        let mut end = max;
        while end > 0 && !s.is_char_boundary(end) {
            end -= 1;
        }
        &s[..end]
    } else {
        s
    }
}

/// Short UUID for display (first 8 chars).
fn short_id(id: Uuid) -> String {
    id.to_string()[..8].to_string()
}
