// r8r: rust port of n8n.io
// Initial author: Michael Cochran <mcochran@cochranblock.org>
// Built for: Tanner Woodrum, intended cofounder
// License: Unlicense (public domain dedication, see UNLICENSE)
//
// `tmux_pane` — generic tmux send-keys / capture-pane driver. Lets a
// workflow talk to any interactive process running in a tmux pane:
// REPLs, build watchers, log tails, server consoles, AI CLIs, etc.
// `anthropic_tmux_tui` is the AI-specific convenience binding; this
// node is the unconstrained primitive.
//
// Operations:
//   send_and_capture (default) — send keys, poll until output stabilizes,
//                                return the diff against pre-send baseline
//   send                       — send keys, return immediately
//   capture                    — capture pane content, no send
//   send_command               — send keys followed by Enter, no wait
//
// Common parameters (addressing — pick ONE style):
//   target       full tmux target string ("session", "session:window",
//                or "session:window.pane"). Wins if set.
//   session      tmux session name (required if `target` not set)
//   window       window name or index (optional, default 0 — only
//                included when set explicitly)
//   pane         pane index (optional, default 0 — only included when
//                window is also set)
//
//   keys         text to send. By default sent literally (`-l`); set
//                `as_key_name = true` to send key names like "Enter",
//                "C-c", "F1"
//   timeout_ms   send_and_capture: max wait (default 10000)
//   stable_ms    send_and_capture: idle window for "done" (default 1000)
//   poll_ms      send_and_capture: capture poll cadence (default 200)
//   strip_ansi   strip ESC sequences from captured output (default true)
//   history      capture-pane -S: history lines to include (default 1000,
//                use 0 for visible-only)

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use serde_json::json;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::process::Command;

pub struct TmuxPaneNode;

static META: NodeMetadata = NodeMetadata {
    name: "tmux_pane",
    display_name: "Tmux Pane",
    version: &[1],
    group: &["transform"],
    description: "Send keys to and capture output from a tmux pane",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

#[async_trait]
impl Node for TmuxPaneNode {
    fn metadata(&self) -> &'static NodeMetadata { &META }

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let target = build_target(ctx)?;
        let operation = ctx.node_def.parameters.get("operation")
            .and_then(|v| v.as_str()).unwrap_or("send_and_capture").to_string();
        let strip = ctx.node_def.parameters.get("strip_ansi")
            .and_then(|v| v.as_bool()).unwrap_or(true);
        let history = ctx.node_def.parameters.get("history")
            .and_then(|v| v.as_i64()).unwrap_or(1000);

        let result = match operation.as_str() {
            "capture" => {
                let captured = capture_pane(&target, history).await?;
                let captured = if strip { strip_ansi(&captured) } else { captured };
                json!({ "target": target, "captured": captured })
            }
            "send" => {
                let keys = ctx.param_str("keys")?;
                let as_key_name = ctx.node_def.parameters.get("as_key_name")
                    .and_then(|v| v.as_bool()).unwrap_or(false);
                send_keys(&target, &keys, as_key_name).await?;
                json!({ "target": target, "sent": keys })
            }
            "send_command" => {
                let keys = ctx.param_str("keys")?;
                send_keys(&target, &keys, false).await?;
                send_keys(&target, "Enter", true).await?;
                json!({ "target": target, "sent": keys })
            }
            "send_and_capture" => {
                let keys = ctx.param_str("keys")?;
                let send_enter = ctx.node_def.parameters.get("send_enter")
                    .and_then(|v| v.as_bool()).unwrap_or(true);
                let timeout_ms = ctx.node_def.parameters.get("timeout_ms")
                    .and_then(|v| v.as_u64()).unwrap_or(10_000);
                let stable_ms = ctx.node_def.parameters.get("stable_ms")
                    .and_then(|v| v.as_u64()).unwrap_or(1000);
                let poll_ms = ctx.node_def.parameters.get("poll_ms")
                    .and_then(|v| v.as_u64()).unwrap_or(200);

                let baseline = capture_pane(&target, history).await?;
                send_keys(&target, &keys, false).await?;
                if send_enter { send_keys(&target, "Enter", true).await?; }

                let started = Instant::now();
                let stable = Duration::from_millis(stable_ms);
                let total = Duration::from_millis(timeout_ms);
                let interval = Duration::from_millis(poll_ms);
                let mut last_seen = capture_pane(&target, history).await?;
                let mut last_change = Instant::now();
                loop {
                    tokio::time::sleep(interval).await;
                    if started.elapsed() > total { break; }
                    let current = capture_pane(&target, history).await?;
                    if current != last_seen {
                        last_seen = current;
                        last_change = Instant::now();
                    } else if last_change.elapsed() >= stable {
                        break;
                    }
                }

                let response = diff_after(&baseline, &last_seen);
                let response = if strip { strip_ansi(&response) } else { response };
                let raw = if strip { strip_ansi(&last_seen) } else { last_seen };
                json!({ "target": target, "sent": keys, "response": response, "raw": raw })
            }
            other => return Err(NodeError::Other(format!("tmux_pane: unknown operation `{}`", other))),
        };

        Ok(vec![vec![NodeItem { json: result, ..Default::default() }]])
    }
}

/// Assemble the tmux target string from explicit `target`, or from
/// `session` + optional `window` + optional `pane`. tmux accepts:
///   "<session>"
///   "<session>:<window>"
///   "<session>:<window>.<pane>"
fn build_target(ctx: &ExecuteCtx<'_>) -> Result<String, NodeError> {
    if let Ok(t) = ctx.param_str("target") {
        if !t.is_empty() { return Ok(t); }
    }
    let session = ctx.param_str("session")
        .map_err(|_| NodeError::MissingParameter("target or session".into()))?;
    let window = ctx.node_def.parameters.get("window")
        .and_then(|v| match v {
            serde_json::Value::String(s) => Some(s.clone()),
            serde_json::Value::Number(n) => Some(n.to_string()),
            _ => None,
        });
    let pane = ctx.node_def.parameters.get("pane")
        .and_then(|v| match v {
            serde_json::Value::String(s) => Some(s.clone()),
            serde_json::Value::Number(n) => Some(n.to_string()),
            _ => None,
        });
    Ok(match (window, pane) {
        (Some(w), Some(p)) => format!("{}:{}.{}", session, w, p),
        (Some(w), None)    => format!("{}:{}", session, w),
        (None, Some(p))    => format!("{}:0.{}", session, p),
        (None, None)       => session,
    })
}

async fn capture_pane(target: &str, history: i64) -> Result<String, NodeError> {
    let mut args: Vec<String> = vec!["capture-pane".into(), "-p".into(), "-t".into(), target.into()];
    if history > 0 {
        args.push("-S".into());
        args.push(format!("-{}", history));
    }
    let out = Command::new("tmux").args(&args).output().await
        .map_err(|e| NodeError::Other(format!("tmux capture-pane: {}", e)))?;
    if !out.status.success() {
        return Err(NodeError::Other(format!(
            "tmux capture-pane failed: {}",
            String::from_utf8_lossy(&out.stderr).trim()
        )));
    }
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

async fn send_keys(target: &str, value: &str, is_key_name: bool) -> Result<(), NodeError> {
    let mut cmd = Command::new("tmux");
    if is_key_name {
        cmd.args(["send-keys", "-t", target, value]);
    } else {
        cmd.args(["send-keys", "-t", target, "-l", value]);
    }
    let out = cmd.output().await
        .map_err(|e| NodeError::Other(format!("tmux send-keys: {}", e)))?;
    if !out.status.success() {
        return Err(NodeError::Other(format!(
            "tmux send-keys failed: {}",
            String::from_utf8_lossy(&out.stderr).trim()
        )));
    }
    Ok(())
}

fn diff_after(baseline: &str, current: &str) -> String {
    let base_lines: Vec<&str> = baseline.lines().collect();
    let cur_lines: Vec<&str> = current.lines().collect();
    let mut shared = 0;
    while shared < base_lines.len()
        && shared < cur_lines.len()
        && base_lines[shared] == cur_lines[shared]
    {
        shared += 1;
    }
    cur_lines[shared..].join("\n")
}

fn strip_ansi(s: &str) -> String {
    let bytes = s.as_bytes();
    let mut out = String::with_capacity(s.len());
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == 0x1B && i + 1 < bytes.len() {
            let next = bytes[i + 1];
            if next == b'[' {
                let mut j = i + 2;
                while j < bytes.len() && !(bytes[j] as char).is_ascii_alphabetic() { j += 1; }
                i = j.saturating_add(1).min(bytes.len());
                continue;
            } else if next == b']' {
                let mut j = i + 2;
                while j < bytes.len() && bytes[j] != 0x07 { j += 1; }
                i = j.saturating_add(1).min(bytes.len());
                continue;
            }
        }
        out.push(bytes[i] as char);
        i += 1;
    }
    out
}

inventory::submit! { NodeFactory::new("tmux_pane", || Arc::new(TmuxPaneNode)) }
