use std::collections::{BTreeMap, BTreeSet};

use crate::cli::{Cli, DEFAULT_DEV_PORTS};
use crate::error::PortKillerError;
use crate::models::{JsonExecutionReport, KillResult, PortStatus, ProcessInfo};
use crate::{output, platform, Result};

pub fn run(cli: Cli) -> Result<()> {
    let ports = resolve_ports(&cli)?;
    let statuses = inspect_ports(&ports)?;

    if cli.json {
        if should_kill(&cli) {
            let results = kill_processes(&statuses, cli.force);
            let report = JsonExecutionReport { statuses, results };
            output::write_json_execution(&report)?;
            return ensure_kills_succeeded(&report.results);
        } else {
            output::write_json_statuses(&statuses)?;
        }

        return Ok(());
    }

    output::print_statuses(&statuses, cli.dry_run);

    if occupied_process_count(&statuses) == 0 || !should_kill(&cli) {
        return Ok(());
    }

    if cli.yes || output::confirm(&kill_prompt(&statuses))? {
        let results = kill_processes(&statuses, cli.force);

        for result in &results {
            if result.success {
                println!("Killed process {}.", result.process.pid);
            }
        }

        ensure_kills_succeeded(&results)?;
    } else {
        println!("Cancelled.");
    }

    Ok(())
}

fn resolve_ports(cli: &Cli) -> Result<Vec<u16>> {
    let mut ports = BTreeSet::new();

    if cli.dev {
        ports.extend(DEFAULT_DEV_PORTS.iter().copied());
    }

    ports.extend(cli.ports.iter().copied());

    if ports.is_empty() {
        return Err(PortKillerError::InvalidArguments(
            "provide at least one port or use --dev.".to_string(),
        ));
    }

    if let Some(port) = ports.iter().find(|port| **port == 0) {
        return Err(PortKillerError::InvalidPort(*port));
    }

    Ok(ports.into_iter().collect())
}

fn inspect_ports(ports: &[u16]) -> Result<Vec<PortStatus>> {
    ports
        .iter()
        .map(|port| {
            platform::find_processes_by_port(*port)
                .map(|processes| PortStatus::new(*port, processes))
        })
        .collect()
}

fn should_kill(cli: &Cli) -> bool {
    if cli.dry_run || cli.list {
        return false;
    }

    if cli.json {
        return cli.yes;
    }

    true
}

fn occupied_process_count(statuses: &[PortStatus]) -> usize {
    unique_processes(statuses).len()
}

fn kill_prompt(statuses: &[PortStatus]) -> String {
    let count = occupied_process_count(statuses);

    if count == 1 {
        "Kill this process?".to_string()
    } else {
        format!("Kill these {count} processes?")
    }
}

fn kill_processes(statuses: &[PortStatus], force: bool) -> Vec<KillResult> {
    unique_processes(statuses)
        .into_values()
        .map(|process| match platform::kill_process(process.pid, force) {
            Ok(()) => KillResult {
                process,
                success: true,
                message: None,
            },
            Err(error) => {
                let message = match error {
                    PortKillerError::KillFailed { reason, .. } => reason,
                    other => other.to_string(),
                };

                KillResult {
                    process,
                    success: false,
                    message: Some(message),
                }
            }
        })
        .collect()
}

fn ensure_kills_succeeded(results: &[KillResult]) -> Result<()> {
    if let Some(result) = results.iter().find(|result| !result.success) {
        let reason = result
            .message
            .clone()
            .unwrap_or_else(|| "unknown error".to_string());

        if reason.to_ascii_lowercase().contains("permission denied") {
            return Err(PortKillerError::PermissionDenied {
                pid: result.process.pid,
            });
        }

        return Err(PortKillerError::KillFailed {
            pid: result.process.pid,
            reason,
        });
    }

    Ok(())
}

fn unique_processes(statuses: &[PortStatus]) -> BTreeMap<u32, ProcessInfo> {
    let mut processes = BTreeMap::new();

    for status in statuses {
        for process in &status.processes {
            processes
                .entry(process.pid)
                .or_insert_with(|| process.clone());
        }
    }

    processes
}

#[cfg(test)]
mod tests {
    use clap::Parser;

    use super::{kill_prompt, resolve_ports, should_kill};
    use crate::cli::{Cli, DEFAULT_DEV_PORTS};
    use crate::models::{PortStatus, ProcessInfo};

    #[test]
    fn resolves_explicit_ports() {
        let cli = Cli::parse_from(["port-killer", "3000", "5173"]);
        assert_eq!(resolve_ports(&cli).unwrap(), vec![3000, 5173]);
    }

    #[test]
    fn includes_default_dev_ports() {
        let cli = Cli::parse_from(["port-killer", "--dev", "3002"]);
        let ports = resolve_ports(&cli).unwrap();

        assert!(DEFAULT_DEV_PORTS.iter().all(|port| ports.contains(port)));
        assert!(ports.contains(&3002));
    }

    #[test]
    fn rejects_port_zero() {
        let cli = Cli::parse_from(["port-killer", "0"]);
        assert!(resolve_ports(&cli).is_err());
    }

    #[test]
    fn dry_run_does_not_kill() {
        let cli = Cli::parse_from(["port-killer", "3000", "--dry-run", "--yes"]);
        assert!(!should_kill(&cli));
    }

    #[test]
    fn list_does_not_kill() {
        let cli = Cli::parse_from(["port-killer", "3000", "--list", "--yes"]);
        assert!(!should_kill(&cli));
    }

    #[test]
    fn prompts_for_normal_text_mode() {
        let cli = Cli::parse_from(["port-killer", "3000"]);
        assert!(should_kill(&cli));
    }

    #[test]
    fn json_requires_yes_to_kill() {
        let cli = Cli::parse_from(["port-killer", "3000", "--json"]);
        assert!(!should_kill(&cli));

        let cli = Cli::parse_from(["port-killer", "3000", "--json", "--yes"]);
        assert!(should_kill(&cli));
    }

    #[test]
    fn prompt_names_single_or_multiple_processes() {
        let process = ProcessInfo {
            pid: 123,
            name: "node".to_string(),
            command: "npm run dev".to_string(),
            port: 3000,
        };
        let single = vec![PortStatus::new(3000, vec![process.clone()])];
        let multiple = vec![PortStatus::new(
            3000,
            vec![
                process,
                ProcessInfo {
                    pid: 456,
                    name: "vite".to_string(),
                    command: "vite".to_string(),
                    port: 5173,
                },
            ],
        )];

        assert_eq!(kill_prompt(&single), "Kill this process?");
        assert_eq!(kill_prompt(&multiple), "Kill these 2 processes?");
    }
}
