//! Send task notifications to Matrix room
//!
//! This command allows agents to summon humans when blocked or need review
//! by sending nicely formatted task details to a Matrix room.

use anyhow::{Context, Result};
use serde::Serialize;
use std::path::Path;
use workgraph::MatrixConfig;
use workgraph::graph::{Status, Task};
use workgraph::parser::load_graph;

// Use the appropriate Matrix client based on the enabled feature
#[cfg(feature = "matrix")]
use workgraph::MatrixClient;
#[cfg(all(feature = "matrix-lite", not(feature = "matrix")))]
use workgraph::MatrixClientLite as MatrixClient;

use super::graph_path;

/// JSON output for notify command
#[derive(Debug, Serialize)]
struct NotifyResult {
    task_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    room: Option<String>,
    sent: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

/// Helper to output errors consistently (JSON or plain text)
fn output_error(json: bool, task_id: &str, room: Option<&str>, error: &str) -> Result<()> {
    if json {
        let output = NotifyResult {
            task_id: task_id.to_string(),
            room: room.map(std::string::ToString::to_string),
            sent: false,
            error: Some(error.to_string()),
        };
        println!("{}", serde_json::to_string_pretty(&output)?);
        Ok(())
    } else {
        anyhow::bail!("{}", error)
    }
}

pub fn run(
    dir: &Path,
    task_id: &str,
    room: Option<&str>,
    message: Option<&str>,
    json: bool,
) -> Result<()> {
    let path = graph_path(dir);

    if !path.exists() {
        return output_error(
            json,
            task_id,
            None,
            "WG not initialized. Run 'wg init' first.",
        );
    }

    // Load task
    let graph = match load_graph(&path) {
        Ok(g) => g,
        Err(e) => {
            return output_error(json, task_id, None, &format!("Failed to load graph: {}", e));
        }
    };
    let task = match graph.get_task(task_id) {
        Some(t) => t,
        None => {
            return output_error(
                json,
                task_id,
                None,
                &format!("Task '{}' not found", task_id),
            );
        }
    };

    // Load Matrix config
    let matrix_config = match MatrixConfig::load() {
        Ok(c) => c,
        Err(e) => {
            return output_error(
                json,
                task_id,
                None,
                &format!("Failed to load Matrix config: {}", e),
            );
        }
    };

    if !matrix_config.has_credentials() {
        return output_error(
            json,
            task_id,
            None,
            "Matrix not configured. Run 'wg config --matrix' to set up credentials. \
             Required: homeserver_url, username, and either password or access_token",
        );
    }

    // Determine room to send to
    let target_room = room
        .map(std::string::ToString::to_string)
        .or(matrix_config.default_room.clone());
    let target_room = match target_room {
        Some(r) => r,
        None => {
            return output_error(
                json,
                task_id,
                None,
                "No room specified. Use --room or configure a default room with 'wg config --room <room>'",
            );
        }
    };

    // Build the notification message
    let (plain_text, html) = format_notification(task, message);

    // Send via Matrix
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("Failed to create runtime")?;

    let result = rt.block_on(async {
        send_notification(dir, &matrix_config, &target_room, &plain_text, &html).await
    });

    match result {
        Ok(()) => {
            if json {
                let output = NotifyResult {
                    task_id: task_id.to_string(),
                    room: Some(target_room),
                    sent: true,
                    error: None,
                };
                println!("{}", serde_json::to_string_pretty(&output)?);
            } else {
                println!("Notification sent to {}", target_room);
            }
            Ok(())
        }
        Err(e) => {
            if json {
                let output = NotifyResult {
                    task_id: task_id.to_string(),
                    room: Some(target_room),
                    sent: false,
                    error: Some(e.to_string()),
                };
                println!("{}", serde_json::to_string_pretty(&output)?);
                Ok(())
            } else {
                Err(e)
            }
        }
    }
}

async fn send_notification(
    dir: &Path,
    config: &MatrixConfig,
    room: &str,
    plain_text: &str,
    html: &str,
) -> Result<()> {
    let client = MatrixClient::new(dir, config)
        .await
        .context("Failed to connect to Matrix")?;

    // Try to join the room first (in case we're not in it)
    if let Err(e) = client.join_room(room).await {
        eprintln!("Warning: failed to join room {}: {}", room, e);
    }

    // Send the formatted message
    client
        .send_html_message(room, plain_text, html)
        .await
        .context("Failed to send notification")?;

    Ok(())
}

/// Format the notification message for a task
/// Returns (plain_text, html) tuple
fn format_notification(task: &Task, custom_message: Option<&str>) -> (String, String) {
    let status_emoji = match task.status {
        Status::Open => "📋",
        Status::InProgress => "🔄",
        Status::Done => "✅",
        Status::Blocked => "🚫",
        Status::Failed => "❌",
        Status::Abandoned => "🗑️",
        Status::Waiting => "⏸️",
        Status::PendingValidation => "🔍",
        Status::PendingEval => "🔍",
        Status::FailedPendingEval => "⚠️",
        Status::Incomplete => "🔁",
    };

    let status_str = task.status.to_string();

    // Build plain text version
    let mut plain = String::new();

    // Custom message first if provided
    if let Some(msg) = custom_message {
        plain.push_str(msg);
        plain.push_str("\n\n");
    }

    plain.push_str(&format!(
        "{} Task: {} ({})\n",
        status_emoji, task.title, task.id
    ));
    plain.push_str(&format!("Status: {}\n", status_str));

    if let Some(ref desc) = task.description {
        plain.push_str(&format!("\nDescription:\n{}\n", desc));
    }

    if let Some(ref assigned) = task.assigned {
        plain.push_str(&format!("\nAssigned to: {}\n", assigned));
    }

    // Show blockers for blocked/failed tasks
    if !task.after.is_empty() {
        plain.push_str(&format!("\nAfter: {}\n", task.after.join(", ")));
    }

    if let Some(ref reason) = task.failure_reason {
        plain.push_str(&format!("\nFailure reason: {}\n", reason));
    }

    // Action hints
    plain.push_str("\n---\n");
    plain.push_str("Reply with: claim | done | input <info> | help\n");

    // Build HTML version
    let mut html = String::new();

    // Custom message first if provided
    if let Some(msg) = custom_message {
        html.push_str(&format!(
            "<p><strong>{}</strong></p>",
            escape_html(msg).replace('\n', "<br>")
        ));
    }

    html.push_str(&format!(
        "<h4>{} {} <code>{}</code></h4>",
        status_emoji,
        escape_html(&task.title),
        escape_html(&task.id)
    ));

    html.push_str(&format!("<p><strong>Status:</strong> {}</p>", status_str));

    if let Some(ref desc) = task.description {
        html.push_str(&format!(
            "<p><strong>Description:</strong></p><blockquote>{}</blockquote>",
            escape_html(desc).replace('\n', "<br>")
        ));
    }

    if let Some(ref assigned) = task.assigned {
        html.push_str(&format!(
            "<p><strong>Assigned to:</strong> {}</p>",
            escape_html(assigned)
        ));
    }

    // Show blockers
    if !task.after.is_empty() {
        let blockers: Vec<String> = task
            .after
            .iter()
            .map(|b| format!("<code>{}</code>", escape_html(b)))
            .collect();
        html.push_str(&format!(
            "<p><strong>After:</strong> {}</p>",
            blockers.join(", ")
        ));
    }

    if let Some(ref reason) = task.failure_reason {
        html.push_str(&format!(
            "<p><strong>Failure reason:</strong> <em>{}</em></p>",
            escape_html(reason)
        ));
    }

    // Action hints
    html.push_str("<hr>");
    html.push_str("<p><em>Reply with:</em> <code>claim</code> | <code>done</code> | <code>input &lt;info&gt;</code> | <code>help</code></p>");

    (plain, html)
}

fn escape_html(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

#[cfg(test)]
mod tests {
    use super::*;
    use workgraph::graph::{PRIORITY_DEFAULT, Task};

    fn make_test_task() -> Task {
        Task {
            id: "test-task".to_string(),
            title: "Test Task Title".to_string(),
            description: Some("This is a test description".to_string()),
            status: Status::InProgress,
            priority: PRIORITY_DEFAULT,
            assigned: Some("agent-1".to_string()),
            estimate: None,
            before: vec![],
            after: vec!["blocker-1".to_string()],
            requires: vec![],
            tags: vec![],
            skills: vec![],
            inputs: vec![],
            deliverables: vec![],
            artifacts: vec![],
            exec: None,
            timeout: None,
            not_before: None,
            created_at: None,
            started_at: None,
            completed_at: None,
            last_interaction_at: None,
            log: vec![],
            retry_count: 0,
            max_retries: None,
            failure_reason: None,
            failure_class: None,
            model: None,
            provider: None,
            endpoint: None,
            command_argv: vec![],
            working_dir: None,
            executor_preset_name: None,
            verify: None,
            verify_timeout: None,
            agent: None,
            loop_iteration: 0,
            last_iteration_completed_at: None,
            cycle_failure_restarts: 0,
            ready_after: None,
            paused: false,
            visibility: "internal".to_string(),
            context_scope: None,
            exec_mode: None,
            cycle_config: None,
            token_usage: None,
            session_id: None,
            wait_condition: None,
            checkpoint: None,
            triage_count: 0,
            resurrection_count: 0,
            last_resurrected_at: None,
            validation: None,
            validation_commands: vec![],
            validator_agent: None,
            validator_model: None,
            gate_attempts: 0,
            test_required: false,
            rejection_count: 0,
            max_rejections: None,
            verify_failures: 0,
            rescue_count: 0,
            rescued: false,
            meta_eval_attempts: 0,
            spawn_failures: 0,
            dispatch_count: 0,
            tier: None,
            no_tier_escalation: false,
            tried_models: vec![],
            superseded_by: vec![],
            supersedes: None,
            unplaced: false,
            place_near: vec![],
            place_before: vec![],
            independent: false,
            iteration_round: 0,
            iteration_anchor: None,
            iteration_parent: None,
            iteration_config: None,
            cron_schedule: None,
            cron_enabled: false,
            last_cron_fire: None,
            next_cron_fire: None,
        }
    }

    #[test]
    fn test_format_notification_basic() {
        let task = make_test_task();
        let (plain, html) = format_notification(&task, None);

        assert!(plain.contains("Test Task Title"));
        assert!(plain.contains("test-task"));
        assert!(plain.contains("in-progress"));
        assert!(plain.contains("agent-1"));
        assert!(plain.contains("blocker-1"));

        assert!(html.contains("Test Task Title"));
        assert!(html.contains("<code>test-task</code>"));
    }

    #[test]
    fn test_format_notification_with_custom_message() {
        let task = make_test_task();
        let (plain, html) = format_notification(&task, Some("Need help with this!"));

        assert!(plain.starts_with("Need help with this!"));
        assert!(html.contains("Need help with this!"));
    }

    #[test]
    fn test_format_notification_failed_task() {
        let mut task = make_test_task();
        task.status = Status::Failed;
        task.failure_reason = Some("Build failed".to_string());

        let (plain, html) = format_notification(&task, None);

        assert!(plain.contains("❌"));
        assert!(plain.contains("Build failed"));
        assert!(html.contains("Build failed"));
    }

    #[test]
    fn test_escape_html() {
        assert_eq!(escape_html("<script>"), "&lt;script&gt;");
        assert_eq!(escape_html("a & b"), "a &amp; b");
        assert_eq!(escape_html("\"quoted\""), "&quot;quoted&quot;");
    }

    #[test]
    fn test_action_hints_included() {
        let task = make_test_task();
        let (plain, html) = format_notification(&task, None);

        assert!(plain.contains("claim"));
        assert!(plain.contains("done"));
        assert!(plain.contains("input"));
        assert!(html.contains("<code>claim</code>"));
    }
}
