use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;

use anyhow::Context;

const MARKER_BEGIN: &str = "# --- layover begin ---";
const MARKER_END: &str = "# --- layover end ---";

pub fn run(server: &str, hosts: &[String], install: bool, no_tls: bool) -> anyhow::Result<()> {
    // Reject newlines to prevent SSH config directive injection
    if server.contains(['\n', '\r']) {
        anyhow::bail!("invalid server address");
    }
    for h in hosts {
        if h.contains(['\n', '\r']) {
            anyhow::bail!("invalid host pattern");
        }
    }
    let snippet = generate_snippet(server, hosts, no_tls);

    if install {
        install_snippet(&snippet)?;
        eprintln!("layover: SSH config installed to ~/.ssh/config");
    } else {
        println!("{snippet}");
        eprintln!("layover: add the above to ~/.ssh/config, or re-run with --install");
    }

    Ok(())
}

fn generate_snippet(server: &str, hosts: &[String], no_tls: bool) -> String {
    let no_tls_flag = if no_tls { " --no-tls" } else { "" };
    let proxy_cmd = format!("layover connect %h:%p --server {server}{no_tls_flag}");

    let mut snippet = String::new();

    if hosts.is_empty() {
        // Wildcard mode: exclude the relay server to prevent loops
        let relay_host = server.rsplit_once(':').map(|(h, _)| h).unwrap_or(server);
        snippet.push_str(&format!("Host {relay_host}\n"));
        snippet.push_str("    ProxyCommand none\n\n");
        snippet.push_str(&format!("Host *\n"));
        snippet.push_str(&format!("    ProxyCommand {proxy_cmd}\n"));
    } else {
        let host_pattern = hosts.join(" ");
        snippet.push_str(&format!("Host {host_pattern}\n"));
        snippet.push_str(&format!("    ProxyCommand {proxy_cmd}\n"));
    }

    snippet
}

fn install_snippet(snippet: &str) -> anyhow::Result<()> {
    let ssh_dir = home_dir()?.join(".ssh");
    fs::create_dir_all(&ssh_dir).context("failed to create ~/.ssh")?;

    let config_path = ssh_dir.join("config");
    let block = format!("{MARKER_BEGIN}\n{snippet}{MARKER_END}\n");

    if config_path.exists() {
        let existing = fs::read_to_string(&config_path).context("failed to read ~/.ssh/config")?;

        if let (Some(start), Some(end)) = (existing.find(MARKER_BEGIN), existing.find(MARKER_END)) {
            // Replace existing layover block
            let end = end + MARKER_END.len();
            // Trim trailing newline after marker if present
            let end = if existing[end..].starts_with('\n') {
                end + 1
            } else {
                end
            };
            let mut updated = String::with_capacity(existing.len());
            updated.push_str(&existing[..start]);
            updated.push_str(&block);
            updated.push_str(&existing[end..]);
            fs::write(&config_path, updated).context("failed to write ~/.ssh/config")?;
            eprintln!("layover: replaced existing layover block in ~/.ssh/config");
            return Ok(());
        }
    }

    // Append to config
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&config_path)
        .context("failed to open ~/.ssh/config")?;

    // Add a blank line separator if the file isn't empty
    if config_path.metadata().map(|m| m.len() > 0).unwrap_or(false) {
        writeln!(file)?;
    }
    write!(file, "{block}")?;

    // Ensure correct permissions
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&config_path, fs::Permissions::from_mode(0o600))?;
    }

    Ok(())
}

fn home_dir() -> anyhow::Result<PathBuf> {
    dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not determine home directory"))
}
