use anyhow::Context;
use csv::{ReaderBuilder, WriterBuilder};
use is_terminal::IsTerminal;
use plotters::prelude::*;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command as StdCommand;
use std::sync::{
    Arc,
    atomic::{AtomicBool, Ordering},
};
use std::time::{Duration, Instant};
use sysinfo::{Pid, ProcessRefreshKind, System};

#[cfg(feature = "rerun")]
use rerun as rr;

const PLOT_WIDTH: u32 = 800;
const PLOT_HEIGHT: u32 = 600;

#[derive(Debug, Clone)]
pub struct ProbeConfig<'a> {
    pub target: &'a str,
    pub interval_secs: f64,
    pub out_dir: &'a Path,
    pub strict: bool,
    pub json_output: bool,
    pub rerun_output: bool,
}

#[derive(Serialize)]
pub struct ProbeResults {
    pub cpu_name: String,
    pub architecture: String,
    pub gpu_name: Option<String>,
    pub ram_total_gb: f64,
    pub logical_threads: usize,
    pub physical_cores: Option<usize>,

    pub process_cpu_avg: f64,
    pub process_cpu_system_avg: f64,
    pub process_cpu_median: f64,
    pub process_cpu_std_dev: f64,
    pub process_cpu_p95: f64,
    pub process_cpu_max: f64,

    pub process_mem_avg: f64,
    pub process_mem_median: f64,
    pub process_mem_std_dev: f64,
    pub process_mem_p95: f64,
    pub process_mem_max: f64,

    pub system_mem_avg: f64,
    pub cpu_freq_avg: f64,
    pub gpu_freq_avg: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProbeSample {
    time_s: f64,
    process_cpu_percent: f64,
    process_mem_mb: f64,
    sys_mem_percent: f64,
    cpu_freq_mhz: f64,
    gpu_freq_mhz: f64,
}

#[derive(Debug, Clone)]
struct ProbeMetadata {
    cpu_count: f64,
    logical_threads: usize,
    cpu_name: String,
    architecture: String,
    gpu_name: Option<String>,
    ram_total_gb: f64,
    physical_cores: Option<usize>,
}

#[derive(Debug, Clone, Copy)]
struct SummaryStats {
    avg: f64,
    median: f64,
    std_dev: f64,
    p95: f64,
    max: f64,
}

type PlotSeries = Vec<(f64, f64)>;

#[must_use]
pub fn get_gpu_freq() -> f64 {
    #[cfg(target_os = "macos")]
    {
        if let Ok(output) = StdCommand::new("powermetrics")
            .args(["-n", "1", "--samplers", "gpu_power"])
            .output()
        {
            if let Ok(s) = String::from_utf8(output.stdout) {
                for line in s.lines() {
                    if line.contains("GPU active frequency:") || line.contains("GPU freq:") {
                        for part in line.split_whitespace() {
                            if let Ok(freq) = part.parse::<f64>() {
                                return freq;
                            }
                        }
                    }
                }
            }
        }
    }

    #[cfg(target_os = "linux")]
    {
        if let Ok(output) = StdCommand::new("nvidia-smi")
            .args(["--query-gpu=clocks.gr", "--format=csv,noheader,nounits"])
            .output()
            && let Ok(s) = String::from_utf8(output.stdout)
            && let Ok(v) = s.trim().parse::<f64>()
        {
            return v;
        }

        if let Ok(s) = std::fs::read_to_string("/sys/class/drm/card0/gt_cur_freq_mhz")
            && let Ok(v) = s.trim().parse::<f64>()
        {
            return v;
        }
    }

    0.0
}

pub fn get_gpu_name() -> Option<String> {
    #[cfg(target_os = "linux")]
    {
        if let Ok(output) = StdCommand::new("nvidia-smi")
            .args(["--query-gpu=name", "--format=csv,noheader"])
            .output()
            && let Ok(s) = String::from_utf8(output.stdout)
        {
            return s
                .lines()
                .map(str::trim)
                .find(|line| !line.is_empty())
                .map(str::to_string);
        }
    }

    None
}

#[cfg(target_os = "linux")]
fn get_process_cpu_time(pid: u32) -> f64 {
    if let Ok(content) = std::fs::read_to_string(format!("/proc/{pid}/stat")) {
        let parts: Vec<&str> = content.split_whitespace().collect();
        if parts.len() > 14 {
            let utime = parts[13].parse::<f64>().unwrap_or(0.0);
            let stime = parts[14].parse::<f64>().unwrap_or(0.0);
            return (utime + stime) / 100.0;
        }
    }
    0.0
}

#[cfg(target_os = "macos")]
fn get_process_cpu_time(pid: u32) -> f64 {
    #[repr(C)]
    struct proc_taskinfo {
        pti_virtual_size: u64,
        pti_resident_size: u64,
        pti_total_user: u64,
        pti_total_system: u64,
        _padding: [u8; 400],
    }

    unsafe extern "C" {
        fn proc_pidinfo(
            pid: libc::pid_t,
            flavor: libc::c_int,
            arg: u64,
            buffer: *mut libc::c_void,
            buffersize: libc::c_int,
        ) -> libc::c_int;
    }

    let mut info: proc_taskinfo = unsafe { std::mem::zeroed() };
    let size = std::mem::size_of::<proc_taskinfo>() as libc::c_int;

    let res = unsafe {
        proc_pidinfo(
            pid as libc::pid_t,
            4,
            0,
            &mut info as *mut _ as *mut libc::c_void,
            size,
        )
    };
    if res == size {
        let total_ns = info.pti_total_user + info.pti_total_system;
        return total_ns as f64 / 1_000_000_000.0;
    }
    0.0
}

fn calculate_stats(values: &[f64]) -> SummaryStats {
    if values.is_empty() {
        return SummaryStats {
            avg: 0.0,
            median: 0.0,
            std_dev: 0.0,
            p95: 0.0,
            max: 0.0,
        };
    }

    let len_f = values.len() as f64;
    let sum: f64 = values.iter().sum();
    let avg = sum / len_f;

    let variance = values
        .iter()
        .map(|value| {
            let diff = avg - *value;
            diff * diff
        })
        .sum::<f64>()
        / len_f;

    let mut sorted = values.to_vec();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

    let median = if sorted.len().is_multiple_of(2) {
        let mid = sorted.len() / 2;
        f64::midpoint(sorted[mid - 1], sorted[mid])
    } else {
        sorted[sorted.len() / 2]
    };

    let p95_idx = ((sorted.len() - 1) as f64 * 0.95).round() as usize;

    SummaryStats {
        avg,
        median,
        std_dev: variance.sqrt(),
        p95: sorted[p95_idx.min(sorted.len() - 1)],
        max: *sorted.last().unwrap_or(&0.0),
    }
}

fn create_probe_plot(
    filepath: &Path,
    title: &str,
    data: &[(f64, f64)],
    y_label: &str,
) -> anyhow::Result<()> {
    if data.is_empty() {
        return Ok(());
    }

    let root = SVGBackend::new(filepath, (PLOT_WIDTH, PLOT_HEIGHT)).into_drawing_area();
    root.fill(&WHITE)?;

    let start_t = data[0].0;
    let max_t = data.last().map_or(0.0, |(t, _)| *t);
    let max_val = data.iter().fold(0.0, |max: f64, (_, v)| max.max(*v));
    let y_max = if max_val > 0.0 { max_val * 1.1 } else { 1.0 };

    let mut chart = ChartBuilder::on(&root)
        .caption(title, ("sans-serif", 40).into_font())
        .margin(10)
        .x_label_area_size(40)
        .y_label_area_size(60)
        .build_cartesian_2d(0.0..(max_t - start_t), 0.0..y_max)?;

    chart
        .configure_mesh()
        .x_desc("Time (s)")
        .y_desc(y_label)
        .draw()?;

    chart.draw_series(LineSeries::new(
        data.iter().map(|(t, v)| (*t - start_t, *v)),
        &BLUE,
    ))?;

    root.present()?;
    Ok(())
}

#[cfg(feature = "rerun")]
fn visualize_probe_with_rerun(samples: &[ProbeSample]) -> anyhow::Result<()> {
    let rec = rr::RecordingStreamBuilder::new("Probe Analysis").spawn()?;

    for sample in samples {
        rec.set_duration_secs("time", sample.time_s);
        rec.log(
            "probe/process_cpu_percent",
            &rr::Scalars::new([sample.process_cpu_percent]),
        )?;
        rec.log(
            "probe/process_memory_mb",
            &rr::Scalars::new([sample.process_mem_mb]),
        )?;
        rec.log(
            "probe/system_memory_percent",
            &rr::Scalars::new([sample.sys_mem_percent]),
        )?;
        rec.log(
            "probe/cpu_frequency_mhz",
            &rr::Scalars::new([sample.cpu_freq_mhz]),
        )?;
        rec.log(
            "probe/gpu_frequency_mhz",
            &rr::Scalars::new([sample.gpu_freq_mhz]),
        )?;
    }

    Ok(())
}

fn resolve_target_process(sys: &mut System, target: &str, strict: bool) -> Option<(Pid, String)> {
    if strict {
        loop {
            sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
            let mut matched_pids: Vec<Pid> = sys
                .processes()
                .iter()
                .filter_map(|(pid, process)| {
                    let proc_name = process.name().to_string_lossy();
                    if proc_name == target {
                        Some(*pid)
                    } else {
                        None
                    }
                })
                .collect();

            if !matched_pids.is_empty() {
                matched_pids.sort_by_key(|pid| pid.as_u32());
                if let Some(pid) = matched_pids.pop() {
                    return Some((pid, target.to_string()));
                }
            }

            std::thread::sleep(Duration::from_millis(500));
        }
    }

    if let Ok(pid) = target.parse::<usize>() {
        let pid = Pid::from(pid);
        let name = sys
            .process(pid)
            .map(|p| p.name().to_string_lossy().to_string())
            .unwrap_or_else(|| target.to_string());
        return Some((pid, name));
    }

    let mut best_match: Option<(Pid, String)> = None;
    let mut best_score = isize::MIN;
    let mut substring_matches = Vec::new();

    for (pid, process) in sys.processes() {
        let proc_name = process.name().to_string_lossy();
        let proc_name_str = proc_name.as_ref();

        if proc_name_str.contains(target) {
            substring_matches.push((*pid, proc_name_str.to_string()));
        }

        if let Some(m) = sublime_fuzzy::best_match(target, proc_name_str) {
            if m.score() > best_score {
                best_score = m.score();
                best_match = Some((*pid, proc_name_str.to_string()));
            } else if m.score() == best_score
                && let Some((best_pid, _)) = best_match
                && pid.as_u32() > best_pid.as_u32()
            {
                best_match = Some((*pid, proc_name_str.to_string()));
            }
        }
    }

    if !substring_matches.is_empty() {
        substring_matches.sort_by_key(|(pid, _)| pid.as_u32());
        return substring_matches.pop();
    }

    best_match
}

fn collect_probe_metadata(sys: &System) -> ProbeMetadata {
    let cpu_count = sys.cpus().len() as f64;
    let logical_threads = sys.cpus().len();
    let cpu_name = sys
        .cpus()
        .first()
        .map(|cpu| cpu.brand().trim().to_string())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "Unknown".to_string());

    ProbeMetadata {
        cpu_count,
        logical_threads,
        cpu_name,
        architecture: std::env::consts::ARCH.to_string(),
        gpu_name: get_gpu_name(),
        ram_total_gb: sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0,
        physical_cores: System::physical_core_count(),
    }
}

fn record_samples_to_csv(
    csv_path: &Path,
    target_pid: Pid,
    interval_secs: f64,
    stdout_is_terminal: bool,
    stdin_is_terminal: bool,
) -> anyhow::Result<()> {
    let stop_flag = Arc::new(AtomicBool::new(false));
    let worker_stop_flag = Arc::clone(&stop_flag);
    let csv_path = csv_path.to_path_buf();

    let handle = std::thread::spawn(move || {
        let mut writer = match WriterBuilder::new().has_headers(true).from_path(&csv_path) {
            Ok(writer) => writer,
            Err(e) => {
                eprintln!("Error creating CSV writer: {e}");
                worker_stop_flag.store(true, Ordering::SeqCst);
                return;
            }
        };

        let mut sys_thread = System::new_all();
        sys_thread.refresh_cpu_usage();
        sys_thread.refresh_processes_specifics(
            sysinfo::ProcessesToUpdate::All,
            true,
            ProcessRefreshKind::everything(),
        );

        let start_time = Instant::now();
        let mut last_cpu_time = get_process_cpu_time(target_pid.as_u32());
        let mut last_time = Instant::now();

        while !worker_stop_flag.load(Ordering::SeqCst) {
            std::thread::sleep(Duration::from_secs_f64(interval_secs));

            sys_thread.refresh_memory();
            sys_thread.refresh_cpu_usage();
            sys_thread.refresh_processes_specifics(
                sysinfo::ProcessesToUpdate::All,
                true,
                ProcessRefreshKind::everything(),
            );

            let now = Instant::now();
            let current_cpu_time = get_process_cpu_time(target_pid.as_u32());
            let time_delta = now.duration_since(last_time).as_secs_f64();

            let mut proc_cpu = 0.0;
            if time_delta > 0.0 {
                let cpu_delta = current_cpu_time - last_cpu_time;
                proc_cpu = (cpu_delta / time_delta) * 100.0;
            }

            last_cpu_time = current_cpu_time;
            last_time = now;

            let proc_mem_mb = if let Some(process) = sys_thread.process(target_pid) {
                process.memory() as f64 / 1024.0 / 1024.0
            } else {
                if stdout_is_terminal {
                    eprintln!("Process died. Stopping.");
                }
                worker_stop_flag.store(true, Ordering::SeqCst);
                break;
            };

            let total_memory = sys_thread.total_memory() as f64;
            let sys_mem_pct = if total_memory > 0.0 {
                (sys_thread.used_memory() as f64 / total_memory) * 100.0
            } else {
                0.0
            };

            let cpus = sys_thread.cpus();
            let cpu_freq_sum: f64 = cpus.iter().map(|cpu| cpu.frequency() as f64).sum();
            let avg_freq = if cpus.is_empty() {
                0.0
            } else {
                cpu_freq_sum / cpus.len() as f64
            };

            let sample = ProbeSample {
                time_s: start_time.elapsed().as_secs_f64(),
                process_cpu_percent: proc_cpu,
                process_mem_mb: proc_mem_mb,
                sys_mem_percent: sys_mem_pct,
                cpu_freq_mhz: avg_freq,
                gpu_freq_mhz: get_gpu_freq(),
            };

            if let Err(e) = writer.serialize(sample) {
                eprintln!("Error writing CSV row: {e}");
                worker_stop_flag.store(true, Ordering::SeqCst);
                break;
            }
            let _ = writer.flush();
        }
    });

    if stdin_is_terminal {
        if stdout_is_terminal {
            eprintln!("Recording data... Press ENTER to stop recording.");
        }
        let mut dummy = String::new();
        let _ = std::io::stdin().read_line(&mut dummy);
        stop_flag.store(true, Ordering::SeqCst);
    } else {
        if stdout_is_terminal {
            eprintln!("Recording data in non-interactive mode. Waiting for process to exit.");
        }
        while !stop_flag.load(Ordering::SeqCst) {
            std::thread::sleep(Duration::from_millis(200));
        }
    }

    handle
        .join()
        .map_err(|_| anyhow::anyhow!("sampling thread panicked"))?;

    Ok(())
}

fn load_samples(csv_path: &Path) -> anyhow::Result<Vec<ProbeSample>> {
    let mut reader = ReaderBuilder::new()
        .has_headers(true)
        .from_path(csv_path)
        .with_context(|| format!("failed to open probe csv {}", csv_path.display()))?;

    let mut samples = Vec::new();
    for row in reader.deserialize::<ProbeSample>() {
        samples.push(row.context("failed to parse probe csv row")?);
    }

    Ok(samples)
}

fn compute_probe_results(samples: &[ProbeSample], meta: &ProbeMetadata) -> ProbeResults {
    let proc_cpu: Vec<f64> = samples.iter().map(|s| s.process_cpu_percent).collect();
    let proc_mem: Vec<f64> = samples.iter().map(|s| s.process_mem_mb).collect();
    let sys_mem: Vec<f64> = samples.iter().map(|s| s.sys_mem_percent).collect();
    let cpu_freq: Vec<f64> = samples.iter().map(|s| s.cpu_freq_mhz).collect();
    let gpu_freq: Vec<f64> = samples.iter().map(|s| s.gpu_freq_mhz).collect();

    let cpu_stats = calculate_stats(&proc_cpu);
    let mem_stats = calculate_stats(&proc_mem);
    let sys_mem_stats = calculate_stats(&sys_mem);
    let cpu_freq_stats = calculate_stats(&cpu_freq);
    let gpu_freq_stats = calculate_stats(&gpu_freq);

    ProbeResults {
        cpu_name: meta.cpu_name.clone(),
        architecture: meta.architecture.clone(),
        gpu_name: meta.gpu_name.clone(),
        ram_total_gb: meta.ram_total_gb,
        logical_threads: meta.logical_threads,
        physical_cores: meta.physical_cores,

        process_cpu_avg: cpu_stats.avg,
        process_cpu_system_avg: if meta.cpu_count > 0.0 {
            cpu_stats.avg / meta.cpu_count
        } else {
            0.0
        },
        process_cpu_median: cpu_stats.median,
        process_cpu_std_dev: cpu_stats.std_dev,
        process_cpu_p95: cpu_stats.p95,
        process_cpu_max: cpu_stats.max,

        process_mem_avg: mem_stats.avg,
        process_mem_median: mem_stats.median,
        process_mem_std_dev: mem_stats.std_dev,
        process_mem_p95: mem_stats.p95,
        process_mem_max: mem_stats.max,

        system_mem_avg: sys_mem_stats.avg,
        cpu_freq_avg: cpu_freq_stats.avg,
        gpu_freq_avg: gpu_freq_stats.avg,
    }
}

fn build_plot_series(
    samples: &[ProbeSample],
) -> (PlotSeries, PlotSeries, PlotSeries, PlotSeries, PlotSeries) {
    let cpu = samples
        .iter()
        .map(|s| (s.time_s, s.process_cpu_percent))
        .collect();
    let mem = samples
        .iter()
        .map(|s| (s.time_s, s.process_mem_mb))
        .collect();
    let sys_mem = samples
        .iter()
        .map(|s| (s.time_s, s.sys_mem_percent))
        .collect();
    let cpu_freq = samples.iter().map(|s| (s.time_s, s.cpu_freq_mhz)).collect();
    let gpu_freq = samples.iter().map(|s| (s.time_s, s.gpu_freq_mhz)).collect();

    (cpu, mem, sys_mem, cpu_freq, gpu_freq)
}

fn write_probe_plots(samples: &[ProbeSample], dir: &Path) -> anyhow::Result<()> {
    std::fs::create_dir_all(dir)?;

    let (cpu_plot_data, mem_plot_data, sys_mem_plot_data, cpu_freq_plot_data, gpu_freq_plot_data) =
        build_plot_series(samples);

    create_probe_plot(
        &dir.join("probe_cpu.svg"),
        "Process CPU (%) over time",
        &cpu_plot_data,
        "CPU %",
    )?;
    create_probe_plot(
        &dir.join("probe_mem.svg"),
        "Process Memory (MB) over time",
        &mem_plot_data,
        "Memory (MB)",
    )?;
    create_probe_plot(
        &dir.join("probe_system_mem.svg"),
        "System Memory (%) over time",
        &sys_mem_plot_data,
        "System Memory (%)",
    )?;
    create_probe_plot(
        &dir.join("probe_cpu_freq.svg"),
        "CPU Frequency (MHz) over time",
        &cpu_freq_plot_data,
        "CPU Frequency (MHz)",
    )?;
    create_probe_plot(
        &dir.join("probe_gpu_freq.svg"),
        "GPU Frequency (MHz) over time",
        &gpu_freq_plot_data,
        "GPU Frequency (MHz)",
    )?;

    Ok(())
}

fn print_probe_table(results: &ProbeResults, cpu_count: f64) {
    let mut table = comfy_table::Table::new();
    table
        .load_preset(comfy_table::presets::UTF8_FULL)
        .set_header(vec![
            comfy_table::Cell::new("Probe Metric")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
            comfy_table::Cell::new("Average")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
            comfy_table::Cell::new("Median")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
            comfy_table::Cell::new("Std Dev")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
            comfy_table::Cell::new("95th %ile")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
            comfy_table::Cell::new("Max")
                .add_attribute(comfy_table::Attribute::Bold)
                .fg(comfy_table::Color::Cyan),
        ]);

    table.add_row(vec![
        comfy_table::Cell::new("Process CPU (Per Core %)").fg(comfy_table::Color::Yellow),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_avg)),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_median)),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_std_dev)),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_p95)),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_max)),
    ]);

    let cpu_count_safe = if cpu_count > 0.0 { cpu_count } else { 1.0 };
    table.add_row(vec![
        comfy_table::Cell::new("Process CPU (Sys Total %)").fg(comfy_table::Color::Yellow),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_avg / cpu_count_safe)),
        comfy_table::Cell::new(format!(
            "{:.2}",
            results.process_cpu_median / cpu_count_safe
        )),
        comfy_table::Cell::new(format!(
            "{:.2}",
            results.process_cpu_std_dev / cpu_count_safe
        )),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_p95 / cpu_count_safe)),
        comfy_table::Cell::new(format!("{:.2}", results.process_cpu_max / cpu_count_safe)),
    ]);

    table.add_row(vec![
        comfy_table::Cell::new("Process Memory (MB)").fg(comfy_table::Color::Green),
        comfy_table::Cell::new(format!("{:.2}", results.process_mem_avg)),
        comfy_table::Cell::new(format!("{:.2}", results.process_mem_median)),
        comfy_table::Cell::new(format!("{:.2}", results.process_mem_std_dev)),
        comfy_table::Cell::new(format!("{:.2}", results.process_mem_p95)),
        comfy_table::Cell::new(format!("{:.2}", results.process_mem_max)),
    ]);

    println!("\n{table}\n");
    println!("Additional System Stats (Averages):");
    println!("  CPU Name:         {}", results.cpu_name);
    println!("  Architecture:     {}", results.architecture);
    if let Some(name) = &results.gpu_name {
        println!("  GPU Name:         {name}");
    }
    println!("  RAM Total:        {:.2} GB", results.ram_total_gb);
    println!("  Logical Threads:  {}", results.logical_threads);
    if let Some(cores) = results.physical_cores {
        println!("  Physical Cores:   {cores}");
    }
    println!("  System Mem Usage: {:.2} %", results.system_mem_avg);
    println!("  CPU Frequency:    {:.2} MHz", results.cpu_freq_avg);
    if results.gpu_freq_avg > 0.0 {
        println!("  GPU Frequency:    {:.2} MHz", results.gpu_freq_avg);
    }
}

pub fn run_probe(config: ProbeConfig<'_>) -> anyhow::Result<()> {
    std::fs::create_dir_all(config.out_dir)?;

    let stdout_is_terminal = std::io::stdout().is_terminal();
    let stdin_is_terminal = std::io::stdin().is_terminal();
    let non_interactive = !stdout_is_terminal || !stdin_is_terminal;

    if config.strict && stdout_is_terminal {
        eprintln!(
            "Strict mode: waiting for an exact match for process name '{}'...",
            config.target
        );
    }

    let mut sys = System::new_all();
    sys.refresh_all();

    let (target_pid, process_name_found) =
        resolve_target_process(&mut sys, config.target, config.strict).ok_or_else(|| {
            anyhow::anyhow!("Could not find process matching '{}'", config.target)
        })?;

    let metadata = collect_probe_metadata(&sys);

    if stdout_is_terminal {
        eprintln!(
            "Monitoring PID: {} ({}) ({} cores total system)",
            target_pid, process_name_found, metadata.cpu_count
        );
        eprintln!("Logging results to: {}", config.out_dir.display());
    }

    let csv_path = config.out_dir.join("probe_log.csv");
    record_samples_to_csv(
        &csv_path,
        target_pid,
        config.interval_secs,
        stdout_is_terminal,
        stdin_is_terminal,
    )?;

    let samples = load_samples(&csv_path)?;
    let results = compute_probe_results(&samples, &metadata);

    let results_json = serde_json::to_string_pretty(&results)?;
    std::fs::write(config.out_dir.join("probe_results.json"), &results_json)?;

    write_probe_plots(&samples, config.out_dir)?;

    if config.json_output || non_interactive {
        println!("{results_json}");
    } else {
        print_probe_table(&results, metadata.cpu_count);
    }

    #[cfg(feature = "rerun")]
    if config.rerun_output {
        visualize_probe_with_rerun(&samples)?;
    }
    #[cfg(not(feature = "rerun"))]
    let _ = config.rerun_output;

    Ok(())
}

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

    #[test]
    fn calculate_stats_handles_empty() {
        let stats = calculate_stats(&[]);
        assert_eq!(stats.avg, 0.0);
        assert_eq!(stats.median, 0.0);
        assert_eq!(stats.std_dev, 0.0);
        assert_eq!(stats.p95, 0.0);
        assert_eq!(stats.max, 0.0);
    }

    #[test]
    fn calculate_stats_basic_values() {
        let stats = calculate_stats(&[1.0, 2.0, 3.0, 4.0]);
        assert!((stats.avg - 2.5).abs() < 1e-9);
        assert!((stats.median - 2.5).abs() < 1e-9);
        assert_eq!(stats.max, 4.0);
    }

    #[test]
    fn sample_csv_roundtrip() {
        let sample = ProbeSample {
            time_s: 1.0,
            process_cpu_percent: 2.0,
            process_mem_mb: 3.0,
            sys_mem_percent: 4.0,
            cpu_freq_mhz: 5.0,
            gpu_freq_mhz: 6.0,
        };

        let mut buffer = Vec::new();
        {
            let mut writer = WriterBuilder::new()
                .has_headers(true)
                .from_writer(&mut buffer);
            writer.serialize(&sample).expect("serialize sample");
            writer.flush().expect("flush sample");
        }

        let mut reader = ReaderBuilder::new()
            .has_headers(true)
            .from_reader(buffer.as_slice());
        let parsed: Vec<ProbeSample> = reader
            .deserialize::<ProbeSample>()
            .collect::<Result<Vec<_>, _>>()
            .expect("deserialize sample");

        assert_eq!(parsed.len(), 1);
        assert_eq!(parsed[0].time_s, 1.0);
        assert_eq!(parsed[0].gpu_freq_mhz, 6.0);
    }
}
