use std::io::{self, Write};

use crate::models::{JsonExecutionReport, PortStatus, ProcessInfo};
use crate::Result;

pub fn print_statuses(statuses: &[PortStatus], dry_run: bool) {
    if dry_run {
        println!("Dry run mode enabled.");
        println!();
    }

    for status in statuses {
        if status.processes.is_empty() {
            println!("Port {} is free.", status.port);
            continue;
        }

        if dry_run {
            println!("Would kill processes on port {}:", status.port);
        } else {
            println!("Port {} is occupied:", status.port);
        }

        println!();
        print_process_table(&status.processes);
        println!();
    }
}

pub fn print_process_table(processes: &[ProcessInfo]) {
    println!("{:<8} {:<20} {:<8} Command", "PID", "Process", "Port");

    for process in processes {
        println!(
            "{:<8} {:<20} {:<8} {}",
            process.pid,
            truncate(&process.name, 20),
            process.port,
            process.command
        );
    }
}

pub fn write_json_statuses(statuses: &[PortStatus]) -> Result<()> {
    let stdout = io::stdout();
    let handle = stdout.lock();

    if statuses.len() == 1 {
        serde_json::to_writer_pretty(handle, &statuses[0])?;
    } else {
        serde_json::to_writer_pretty(handle, statuses)?;
    }

    println!();
    Ok(())
}

pub fn write_json_execution(report: &JsonExecutionReport) -> Result<()> {
    let stdout = io::stdout();
    serde_json::to_writer_pretty(stdout.lock(), report)?;
    println!();
    Ok(())
}

pub fn confirm(prompt: &str) -> Result<bool> {
    print!("{prompt} [y/N] ");
    io::stdout().flush()?;

    let mut answer = String::new();
    io::stdin().read_line(&mut answer)?;

    Ok(matches!(
        answer.trim().to_ascii_lowercase().as_str(),
        "y" | "yes"
    ))
}

fn truncate(input: &str, max_chars: usize) -> String {
    let mut chars = input.chars();
    let truncated: String = chars.by_ref().take(max_chars).collect();

    if chars.next().is_some() {
        format!(
            "{}...",
            truncated
                .chars()
                .take(max_chars.saturating_sub(3))
                .collect::<String>()
        )
    } else {
        truncated
    }
}

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

    #[test]
    fn truncates_long_names() {
        assert_eq!(truncate("very-long-process-name", 10), "very-lo...");
    }

    #[test]
    fn leaves_short_names_unchanged() {
        assert_eq!(truncate("node", 10), "node");
    }
}
