use std::io;
use std::process::Command;

use crate::error::PortKillerError;
use crate::models::ProcessInfo;
use crate::parser::{parse_lsof_output, parse_ss_output, parse_unix_netstat_output};
use crate::Result;

pub fn find_processes_by_port(port: u16) -> Result<Vec<ProcessInfo>> {
    match find_with_lsof(port) {
        Ok(processes) => Ok(processes),
        Err(PortKillerError::CommandNotFound(_)) => {
            find_with_ss(port).or_else(|_| find_with_netstat(port))
        }
        Err(error) => Err(error),
    }
}

pub fn kill_process(pid: u32, force: bool) -> Result<()> {
    let mut command = Command::new("kill");

    if force {
        command.arg("-9");
    }

    let output = command
        .arg(pid.to_string())
        .output()
        .map_err(|source| map_command_error("kill", source))?;

    if output.status.success() {
        Ok(())
    } else {
        let reason = command_failure_reason(&output);

        if command_failed_with_permission_error(&reason) {
            Err(PortKillerError::PermissionDenied { pid })
        } else {
            Err(PortKillerError::KillFailed { pid, reason })
        }
    }
}

fn find_with_lsof(port: u16) -> Result<Vec<ProcessInfo>> {
    let port_filter = format!("-iTCP:{port}");
    let output = Command::new("lsof")
        .args(["-nP", &port_filter, "-sTCP:LISTEN"])
        .output()
        .map_err(|source| map_command_error("lsof", source))?;

    if !output.status.success() && output.stdout.is_empty() {
        return Ok(Vec::new());
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    Ok(with_command_lines(parse_lsof_output(&stdout, port)))
}

fn find_with_ss(port: u16) -> Result<Vec<ProcessInfo>> {
    let output = Command::new("ss")
        .args(["-ltnp"])
        .output()
        .map_err(|source| map_command_error("ss", source))?;

    if !output.status.success() && output.stdout.is_empty() {
        return Ok(Vec::new());
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    Ok(with_command_lines(parse_ss_output(&stdout, port)))
}

fn find_with_netstat(port: u16) -> Result<Vec<ProcessInfo>> {
    let output = Command::new("netstat")
        .args(["-tulpn"])
        .output()
        .map_err(|source| map_command_error("netstat", source))?;

    if !output.status.success() && output.stdout.is_empty() {
        return Ok(Vec::new());
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    Ok(with_command_lines(parse_unix_netstat_output(&stdout, port)))
}

fn process_command_line(pid: u32) -> Option<String> {
    let pid_text = pid.to_string();
    let output = Command::new("ps")
        .args(["-p", &pid_text, "-o", "command="])
        .output()
        .ok()?;

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

    let command = String::from_utf8_lossy(&output.stdout).trim().to_string();
    (!command.is_empty()).then_some(command)
}

fn with_command_lines(mut processes: Vec<ProcessInfo>) -> Vec<ProcessInfo> {
    for process in &mut processes {
        if let Some(command) = process_command_line(process.pid) {
            process.command = command;
        }
    }

    processes
}

fn map_command_error(command: &str, source: io::Error) -> PortKillerError {
    if source.kind() == io::ErrorKind::NotFound {
        PortKillerError::CommandNotFound(command.to_string())
    } else {
        PortKillerError::CommandIo {
            command: command.to_string(),
            source,
        }
    }
}

fn command_failure_reason(output: &std::process::Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();

    if !stderr.is_empty() {
        stderr
    } else if !stdout.is_empty() {
        stdout
    } else {
        output.status.to_string()
    }
}

fn command_failed_with_permission_error(reason: &str) -> bool {
    reason
        .to_ascii_lowercase()
        .contains("operation not permitted")
}
