use std::collections::BTreeMap;

use crate::models::ProcessInfo;

#[cfg_attr(target_os = "windows", allow(dead_code))]
pub(crate) fn parse_lsof_output(output: &str, port: u16) -> Vec<ProcessInfo> {
    let mut processes = BTreeMap::new();

    for line in output.lines().skip(1) {
        if !line.contains("(LISTEN)") || !line_mentions_port(line, port) {
            continue;
        }

        let columns: Vec<&str> = line.split_whitespace().collect();
        if columns.len() < 2 {
            continue;
        }

        let Ok(pid) = columns[1].parse::<u32>() else {
            continue;
        };

        let name = columns[0].to_string();
        insert_process(&mut processes, pid, name, port);
    }

    processes.into_values().collect()
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
pub(crate) fn parse_ss_output(output: &str, port: u16) -> Vec<ProcessInfo> {
    let mut processes = BTreeMap::new();

    for line in output.lines() {
        let columns: Vec<&str> = line.split_whitespace().collect();
        let Some(local_endpoint) = ss_local_endpoint(&columns) else {
            continue;
        };

        if !endpoint_matches_port(local_endpoint, port) {
            continue;
        }

        let Some(pid) = extract_pid_after(line, "pid=") else {
            continue;
        };

        let name = extract_ss_process_name(line).unwrap_or_else(|| format!("pid-{pid}"));
        insert_process(&mut processes, pid, name, port);
    }

    processes.into_values().collect()
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
pub(crate) fn parse_unix_netstat_output(output: &str, port: u16) -> Vec<ProcessInfo> {
    let mut processes = BTreeMap::new();

    for line in output.lines() {
        let columns: Vec<&str> = line.split_whitespace().collect();
        if columns.len() < 7 {
            continue;
        }

        let local_address = columns[3];
        let state = columns[5];
        let pid_program = columns[6];

        if !state.eq_ignore_ascii_case("LISTEN") || !endpoint_matches_port(local_address, port) {
            continue;
        }

        let pid_part = pid_program.split('/').next().unwrap_or(pid_program);
        let Ok(pid) = pid_part.parse::<u32>() else {
            continue;
        };

        let name = pid_program
            .split_once('/')
            .map(|(_, name)| name.to_string())
            .filter(|name| !name.is_empty())
            .unwrap_or_else(|| format!("pid-{pid}"));
        insert_process(&mut processes, pid, name, port);
    }

    processes.into_values().collect()
}

#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn parse_windows_netstat_output(output: &str, port: u16) -> Vec<ProcessInfo> {
    let mut processes = BTreeMap::new();

    for line in output.lines() {
        let columns: Vec<&str> = line.split_whitespace().collect();
        if columns.len() < 5 || !columns[0].eq_ignore_ascii_case("TCP") {
            continue;
        }

        let local_address = columns[1];
        let state = columns[3];
        let pid_text = columns[columns.len() - 1];

        if !state.eq_ignore_ascii_case("LISTENING") || !endpoint_matches_port(local_address, port) {
            continue;
        }

        let Ok(pid) = pid_text.parse::<u32>() else {
            continue;
        };

        insert_process(&mut processes, pid, format!("pid-{pid}"), port);
    }

    processes.into_values().collect()
}

fn insert_process(processes: &mut BTreeMap<u32, ProcessInfo>, pid: u32, name: String, port: u16) {
    processes.entry(pid).or_insert_with(|| ProcessInfo {
        pid,
        command: name.clone(),
        name,
        port,
    });
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
fn ss_local_endpoint<'a>(columns: &'a [&str]) -> Option<&'a str> {
    if columns.first()?.eq_ignore_ascii_case("LISTEN") {
        columns.get(3).copied()
    } else if columns.get(1)?.eq_ignore_ascii_case("LISTEN") {
        columns.get(4).copied()
    } else {
        None
    }
}

fn endpoint_matches_port(endpoint: &str, port: u16) -> bool {
    endpoint.ends_with(&format!(":{port}"))
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
fn line_mentions_port(line: &str, port: u16) -> bool {
    line.split_whitespace()
        .any(|token| endpoint_matches_port(token, port))
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
fn extract_pid_after(input: &str, marker: &str) -> Option<u32> {
    let start = input.find(marker)? + marker.len();
    let digits: String = input[start..]
        .chars()
        .take_while(|character| character.is_ascii_digit())
        .collect();

    digits.parse().ok()
}

#[cfg_attr(target_os = "windows", allow(dead_code))]
fn extract_ss_process_name(input: &str) -> Option<String> {
    let start = input.find("((\"")? + 3;
    let rest = &input[start..];
    let end = rest.find('"')?;
    let name = rest[..end].to_string();

    (!name.is_empty()).then_some(name)
}

#[cfg(test)]
mod tests {
    use super::{
        endpoint_matches_port, parse_lsof_output, parse_ss_output, parse_unix_netstat_output,
        parse_windows_netstat_output,
    };

    #[test]
    fn matches_exact_endpoint_port() {
        assert!(endpoint_matches_port("127.0.0.1:3000", 3000));
        assert!(endpoint_matches_port("[::]:3000", 3000));
        assert!(endpoint_matches_port("*:3000", 3000));
        assert!(!endpoint_matches_port("127.0.0.1:30000", 3000));
    }

    #[test]
    fn parses_lsof_fixture() {
        let processes = parse_lsof_output(
            include_str!("../tests/fixtures/parser/lsof_listen.txt"),
            3000,
        );

        assert_eq!(processes.len(), 1);
        assert_eq!(processes[0].pid, 12345);
        assert_eq!(processes[0].name, "node");
        assert_eq!(processes[0].port, 3000);
    }

    #[test]
    fn parses_ss_fixture_without_fixed_pid_column() {
        let processes =
            parse_ss_output(include_str!("../tests/fixtures/parser/ss_listen.txt"), 3000);

        assert_eq!(processes.len(), 1);
        assert_eq!(processes[0].pid, 12345);
        assert_eq!(processes[0].name, "node");
        assert_eq!(processes[0].port, 3000);
    }

    #[test]
    fn parses_ss_fixture_with_netid_column() {
        let processes = parse_ss_output(
            include_str!("../tests/fixtures/parser/ss_with_netid.txt"),
            5173,
        );

        assert_eq!(processes.len(), 1);
        assert_eq!(processes[0].pid, 23456);
        assert_eq!(processes[0].name, "vite");
        assert_eq!(processes[0].port, 5173);
    }

    #[test]
    fn ignores_ss_rows_without_pid() {
        let processes = parse_ss_output(
            include_str!("../tests/fixtures/parser/ss_without_pid.txt"),
            3000,
        );

        assert!(processes.is_empty());
    }

    #[test]
    fn parses_unix_netstat_fixture() {
        let processes = parse_unix_netstat_output(
            include_str!("../tests/fixtures/parser/unix_netstat_listen.txt"),
            3000,
        );

        assert_eq!(processes.len(), 1);
        assert_eq!(processes[0].pid, 12345);
        assert_eq!(processes[0].name, "node");
        assert_eq!(processes[0].port, 3000);
    }

    #[test]
    fn parses_windows_netstat_fixture_without_matching_remote_port() {
        let processes = parse_windows_netstat_output(
            include_str!("../tests/fixtures/parser/windows_netstat_listen.txt"),
            3000,
        );

        assert_eq!(processes.len(), 1);
        assert_eq!(processes[0].pid, 12345);
        assert_eq!(processes[0].port, 3000);
    }
}
