use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Instant, SystemTime};

use rayon::prelude::*;
use tracing::{debug, trace, warn};

use crate::protocol::{
    AgentSession, AgentStatus, ExecPlan, Provider, ResolveContext, SessionKind, SessionSource,
};

/// Parse macOS `lstart` format: "Day Mon DD HH:MM:SS YYYY" → epoch seconds.
/// Example: "Mon Mar 10 14:30:00 2026"
fn parse_lstart(tokens: &[&str]) -> Option<u64> {
    if tokens.len() != 5 {
        return None;
    }
    // tokens: [Day, Mon, DD, HH:MM:SS, YYYY]
    let month = match tokens[1] {
        "Jan" => 1,
        "Feb" => 2,
        "Mar" => 3,
        "Apr" => 4,
        "May" => 5,
        "Jun" => 6,
        "Jul" => 7,
        "Aug" => 8,
        "Sep" => 9,
        "Oct" => 10,
        "Nov" => 11,
        "Dec" => 12,
        _ => return None,
    };
    let day: u32 = tokens[2].parse().ok()?;
    let year: i64 = tokens[4].parse().ok()?;
    let hms: Vec<&str> = tokens[3].split(':').collect();
    if hms.len() != 3 {
        return None;
    }
    let hour: u32 = hms[0].parse().ok()?;
    let min: u32 = hms[1].parse().ok()?;
    let sec: u32 = hms[2].parse().ok()?;

    // Convert to epoch using a simple calculation (no leap second precision needed)
    fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
        let y = if m <= 2 { y - 1 } else { y };
        let m = m as i64;
        let d = d as i64;
        let era = if y >= 0 { y } else { y - 399 } / 400;
        let yoe = (y - era * 400) as u64;
        let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
        let doe = yoe as i64 * 365 + yoe as i64 / 4 - yoe as i64 / 100 + doy;
        era * 146097 + doe - 719468
    }

    let days = days_from_civil(year, month, day);
    let epoch = days * 86400 + hour as i64 * 3600 + min as i64 * 60 + sec as i64;
    if epoch < 0 {
        return None;
    }
    Some(epoch as u64)
}

const SESSION_PREFIX: &str = "la/";

pub struct TmuxController;

impl TmuxController {
    pub fn detect() -> bool {
        std::env::var("TMUX").is_ok()
    }

    pub fn tmux_available() -> bool {
        Command::new("tmux")
            .arg("-V")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
    }

    pub fn discover_sessions(providers: &[Box<dyn Provider>]) -> Vec<AgentSession> {
        let start = Instant::now();
        let output = match Command::new("tmux")
            .args([
                "list-panes",
                "-a",
                "-F",
                "#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{pane_current_path}\t#{session_created}",
            ])
            .output()
        {
            Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
            Ok(o) => {
                let stderr = String::from_utf8_lossy(&o.stderr);
                warn!(stderr = %stderr, "tmux list-panes failed");
                return Vec::new();
            }
            Err(e) => {
                warn!(err = %e, "tmux list-panes failed");
                return Vec::new();
            }
        };

        // Build process tree once for all panes
        let proc_tree = build_process_tree();

        // Phase 1: collect matched panes (fast, in-memory only)
        struct MatchedPane {
            session_name: String,
            pane_id: String,
            pane_pid: u32,
            matched_pid: Option<u32>,
            matched_start_epoch: Option<u64>,
            pane_path: String,
            session_created: String,
            provider_id: String,
            kind: SessionKind,
            /// All descendant processes of the matched agent pid: (pid, comm).
            process_descendants: Vec<(String, String)>,
        }

        let mut matched: Vec<MatchedPane> = Vec::new();

        for line in output.lines() {
            let parts: Vec<&str> = line.splitn(6, '\t').collect();
            if parts.len() < 5 {
                continue;
            }

            let session_name = parts[0];
            let pane_id = parts[1];
            let pane_pid = parts[2];
            let pane_cmd = parts[3];
            let pane_path = parts[4];
            let session_created = parts.get(5).unwrap_or(&"");

            // Check if pane command directly matches a provider
            let mut matched_provider: Option<String> = None;
            let mut matched_pid: Option<u32> = None;
            let mut matched_start_epoch: Option<u64> = None;
            for provider in providers {
                trace!(pane_cmd, provider_id = %provider.manifest().id, "checking direct match");
                if provider.match_process(pane_cmd) {
                    matched_provider = Some(provider.manifest().id);
                    // Direct match: the pane process IS the agent
                    matched_pid = pane_pid.parse().ok();
                    // Look up start_epoch from process tree for this pid
                    matched_start_epoch = proc_tree
                        .values()
                        .flatten()
                        .find(|(pid, _, _)| pid == pane_pid)
                        .and_then(|(_, _, epoch)| *epoch);
                    break;
                }
            }

            // If pane command doesn't match, check descendant processes via in-memory tree
            if matched_provider.is_none() {
                let descendants = find_descendant_commands(pane_pid, &proc_tree);
                trace!(pane_pid, descendant_count = descendants.len(), "checking descendants");
                'outer: for (child_pid, child_cmd, child_epoch) in &descendants {
                    for provider in providers {
                        trace!(child_cmd, provider_id = %provider.manifest().id, "checking descendant");
                        if provider.match_process(child_cmd) {
                            matched_provider = Some(provider.manifest().id);
                            matched_pid = child_pid.parse().ok();
                            matched_start_epoch = *child_epoch;
                            break 'outer;
                        }
                    }
                }
            }

            let provider_id = match matched_provider {
                Some(id) => id,
                None => continue,
            };

            let kind = if session_name.starts_with(SESSION_PREFIX) {
                SessionKind::Managed
            } else {
                SessionKind::Discovered
            };

            // Collect descendants of the matched agent pid for resolver context
            let process_descendants: Vec<(String, String)> = matched_pid
                .and_then(|pid| {
                    let desc = find_descendant_commands(&pid.to_string(), &proc_tree);
                    if desc.is_empty() { None } else { Some(desc) }
                })
                .unwrap_or_default()
                .into_iter()
                .map(|(pid, comm, _)| (pid, comm))
                .collect();

            matched.push(MatchedPane {
                session_name: session_name.to_string(),
                pane_id: pane_id.to_string(),
                pane_pid: pane_pid.parse().unwrap_or(0),
                matched_pid,
                matched_start_epoch,
                pane_path: pane_path.to_string(),
                session_created: session_created.to_string(),
                provider_id,
                kind,
                process_descendants,
            });
        }

        debug!(count = matched.len(), "matched panes for status detection");

        // Phase 2: parallel capture-pane + git root resolution
        let sessions: Vec<AgentSession> = matched
            .par_iter()
            .map(|m| {
                let ctx = ResolveContext::new(
                    m.pane_pid,
                    m.pane_path.clone(),
                    m.pane_id.clone(),
                    m.matched_start_epoch,
                    m.matched_pid,
                    m.process_descendants.clone(),
                );
                let status = resolve_status(&ctx, providers, &m.provider_id);
                let git_root = resolve_git_root(Path::new(&m.pane_path));
                trace!(pane_id = %m.pane_id, git_root = ?git_root, "resolved git root");

                let started_at = m
                    .session_created
                    .parse::<u64>()
                    .ok()
                    .map(|secs| SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(secs));

                AgentSession {
                    kind: m.kind.clone(),
                    tmux_session: m.session_name.clone(),
                    tmux_pane: m.pane_id.clone(),
                    provider: m.provider_id.clone(),
                    cwd: PathBuf::from(&m.pane_path),
                    status,
                    started_at,
                    source: SessionSource::Local,
                    git_root,
                }
            })
            .collect();

        let elapsed_ms = start.elapsed().as_millis();
        debug!(elapsed_ms, total = sessions.len(), "discovery complete");
        sessions
    }

    pub fn spawn_session(plan: &ExecPlan, provider_id: &str, dir_name: &str) -> anyhow::Result<String> {
        let session_name = format!("{}{}/{}", SESSION_PREFIX, provider_id, dir_name);
        debug!(session = %session_name, provider_id, dir_name, "spawning tmux session");
        let cwd = plan.cwd.as_deref().unwrap_or(".");

        let mut cmd_parts = vec![plan.program.clone()];
        cmd_parts.extend(plan.args.clone());

        let shell_cmd = cmd_parts
            .iter()
            .map(|s| shell_escape(s))
            .collect::<Vec<_>>()
            .join(" ");

        let mut tmux_args = vec![
            "new-session".to_string(),
            "-d".to_string(),
            "-s".to_string(),
            session_name.clone(),
            "-c".to_string(),
            cwd.to_string(),
            "--".to_string(),
        ];

        // Build env wrapper if needed
        if plan.env.is_empty() {
            tmux_args.push("sh".to_string());
            tmux_args.push("-c".to_string());
            tmux_args.push(shell_cmd);
        } else {
            let mut env_parts = vec!["env".to_string()];
            for (k, v) in &plan.env {
                env_parts.push(format!("{k}={v}"));
            }
            env_parts.push("sh".to_string());
            env_parts.push("-c".to_string());
            env_parts.push(shell_cmd);

            let full_cmd = env_parts
                .iter()
                .map(|s| shell_escape(s))
                .collect::<Vec<_>>()
                .join(" ");

            tmux_args.push("sh".to_string());
            tmux_args.push("-c".to_string());
            tmux_args.push(full_cmd);
        }

        let output = Command::new("tmux")
            .args(&tmux_args)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("tmux new-session failed: {stderr}");
        }

        debug!(session = %session_name, "tmux session spawned");
        Ok(session_name)
    }

    pub fn attach_command(session_name: &str) -> Command {
        let mut cmd = Command::new("tmux");
        cmd.args(["attach-session", "-t", session_name]);
        cmd
    }

    /// Capture the full visible content of a tmux pane with ANSI colors.
    pub fn capture_pane(pane_id: &str) -> Option<String> {
        let output = Command::new("tmux")
            .args(["capture-pane", "-p", "-e", "-t", pane_id])
            .output()
            .ok()?;

        if !output.status.success() {
            return None;
        }

        Some(String::from_utf8_lossy(&output.stdout).into_owned())
    }

    /// Send named keys to a tmux pane (e.g. "Enter", "BSpace", "C-c").
    pub fn send_keys(pane_id: &str, keys: &[&str]) -> anyhow::Result<()> {
        let mut args = vec!["send-keys", "-t", pane_id];
        args.extend(keys);

        let output = Command::new("tmux").args(&args).output()?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("tmux send-keys failed: {stderr}");
        }
        Ok(())
    }

    /// Send literal text to a tmux pane (characters sent as-is).
    pub fn send_text(pane_id: &str, text: &str) -> anyhow::Result<()> {
        let output = Command::new("tmux")
            .args(["send-keys", "-t", pane_id, "-l", text])
            .output()?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("tmux send-keys -l failed: {stderr}");
        }
        Ok(())
    }

    pub fn kill_session(session_name: &str) -> anyhow::Result<()> {
        debug!(session = %session_name, "killing tmux session");
        let output = Command::new("tmux")
            .args(["kill-session", "-t", session_name])
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            warn!(session = %session_name, stderr = %stderr, "tmux kill-session failed");
            anyhow::bail!("tmux kill-session failed: {stderr}");
        }

        Ok(())
    }
}

/// Process entry: (pid, comm, start_epoch).
type ProcEntry = (String, String, Option<u64>);

/// Build a process tree from a single `ps -eo` call.
/// Returns ppid → [(pid, comm, start_epoch)].
/// Field order: pid, ppid, lstart (5 tokens: Day Mon DD HH:MM:SS YYYY), comm.
fn build_process_tree() -> HashMap<String, Vec<ProcEntry>> {
    let output = match Command::new("ps")
        .args(["-eo", "pid=,ppid=,lstart=,comm="])
        .output()
    {
        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
        _ => return HashMap::new(),
    };

    let mut tree: HashMap<String, Vec<ProcEntry>> = HashMap::new();
    for line in output.lines() {
        let tokens: Vec<&str> = line.split_whitespace().collect();
        // Minimum: pid + ppid + 5 lstart tokens + 1 comm = 8
        if tokens.len() < 8 {
            continue;
        }
        let pid = tokens[0].to_string();
        let ppid = tokens[1].to_string();
        let start_epoch = parse_lstart(&tokens[2..7]);
        let comm = tokens[7..].join(" ");
        tree.entry(ppid).or_default().push((pid, comm, start_epoch));
    }
    debug!(proc_tree_size = tree.len(), "process tree built");
    tree
}

/// Walk the process tree in-memory (BFS) to find descendant processes.
/// Returns Vec<(pid, comm, start_epoch)> for each descendant.
fn find_descendant_commands(pid: &str, tree: &HashMap<String, Vec<ProcEntry>>) -> Vec<ProcEntry> {
    let mut result = Vec::new();
    let mut queue = vec![pid.to_string()];

    while let Some(current) = queue.pop() {
        if let Some(children) = tree.get(&current) {
            for (child_pid, comm, start_epoch) in children {
                result.push((child_pid.clone(), comm.clone(), *start_epoch));
                queue.push(child_pid.clone());
            }
        }
    }

    result
}

/// Resolve git root directory name for a path. Returns None if not a git repo.
fn resolve_git_root(cwd: &Path) -> Option<String> {
    Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(cwd)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| {
            let full = String::from_utf8_lossy(&o.stdout).trim().to_string();
            Path::new(&full)
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or(full)
        })
}

fn resolve_status(
    ctx: &ResolveContext,
    providers: &[Box<dyn Provider>],
    provider_id: &str,
) -> AgentStatus {
    for provider in providers {
        if provider.manifest().id == provider_id {
            for resolver in provider.resolvers() {
                if let Some(status) = resolver.resolve(ctx) {
                    debug!(pane_id = %ctx.pane_id, provider_id, ?status, "resolved status");
                    return status;
                }
            }
            break;
        }
    }
    AgentStatus::Unknown
}

fn shell_escape(s: &str) -> String {
    if s.chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/')
    {
        return s.to_string();
    }
    format!("'{}'", s.replace('\'', "'\\''"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_shell_escape_simple() {
        assert_eq!(shell_escape("hello"), "hello");
        assert_eq!(shell_escape("/usr/bin/claude"), "/usr/bin/claude");
    }

    #[test]
    fn test_shell_escape_special_chars() {
        assert_eq!(shell_escape("hello world"), "'hello world'");
        assert_eq!(shell_escape("it's"), "'it'\\''s'");
    }

    #[test]
    fn test_parse_tab_separated_line_with_spaces_in_path() {
        let line = "mysession\t%0\t1234\tbash\t/home/user/my project dir\t1700000000";
        let parts: Vec<&str> = line.splitn(6, '\t').collect();
        assert_eq!(parts.len(), 6);
        assert_eq!(parts[0], "mysession");
        assert_eq!(parts[1], "%0");
        assert_eq!(parts[2], "1234");
        assert_eq!(parts[3], "bash");
        assert_eq!(parts[4], "/home/user/my project dir");
        assert_eq!(parts[5], "1700000000");
    }

    #[test]
    fn test_resolve_git_root_returns_dir_name() {
        // This test runs against the actual repo we're in
        let cwd = std::env::current_dir().expect("cwd");
        let result = resolve_git_root(&cwd);
        assert!(result.is_some(), "should detect git root in project dir");
        // Should be just the dir name, not the full path
        let name = result.unwrap();
        assert!(!name.contains('/'), "should be dir name only, got: {name}");
    }

    #[test]
    fn test_resolve_git_root_non_git_dir() {
        let result = resolve_git_root(Path::new("/tmp"));
        assert!(result.is_none(), "non-git dir should return None");
    }
}
